From 86093fcd08697319fb3e7ce2343d8392244c230c Mon Sep 17 00:00:00 2001 From: Vasek - Tom C Date: Wed, 19 Aug 2026 17:41:57 +0200 Subject: [PATCH 1/2] test(runtimes): check that generated modules actually run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other check inspects what generation writes. None loaded the result — so the contract the whole move rests on was verified only by hand: sdk/ resolving as @dagger.io/dagger, the bundle it holds being loadable, the entrypoint importing the user's classes and registering them. A break there is a module that generates cleanly and fails at `dagger call`. Loading a module is enough to force it. A dagger-module.toml module does no codegen at call time, and the TypeScript SDK has no ModuleTypes function, so reading its objects builds the runtime container and executes the generated entrypoint. One fixture per runtime, because this is where they stop being interchangeable: generation differs only in which config file it writes, but node, bun and deno each execute the entrypoint in a different container with a different interpreter. Bun had never been run at all — it was dismissed as taking node's code path, which is true of generation and false of execution. Staged with withChanges rather than written to disk, which also keeps the fixtures free of generated output and sidesteps a module's own .gitignore hiding its generated files from the engine's module context. Confirmed each check fails when the fixture's object is renamed. Signed-off-by: Tom Chauveau --- .dagger/modules/runtimes/dagger.json | 13 ++++ .../modules/runtimes/fixtures/bun/.gitignore | 2 + .../modules/runtimes/fixtures/bun/bun.lock | 0 .../runtimes/fixtures/bun/dagger-module.toml | 5 ++ .../runtimes/fixtures/bun/package.json | 3 + .../runtimes/fixtures/bun/src/index.ts | 9 +++ .../modules/runtimes/fixtures/deno/.gitignore | 2 + .../runtimes/fixtures/deno/dagger-module.toml | 5 ++ .../modules/runtimes/fixtures/deno/deno.json | 5 ++ .../runtimes/fixtures/deno/src/index.ts | 9 +++ .../modules/runtimes/fixtures/node/.gitignore | 2 + .../runtimes/fixtures/node/dagger-module.toml | 5 ++ .../runtimes/fixtures/node/package.json | 3 + .../runtimes/fixtures/node/src/index.ts | 9 +++ .dagger/modules/runtimes/main.dang | 77 +++++++++++++++++++ dagger.toml | 3 + 16 files changed, 152 insertions(+) create mode 100644 .dagger/modules/runtimes/dagger.json create mode 100644 .dagger/modules/runtimes/fixtures/bun/.gitignore create mode 100644 .dagger/modules/runtimes/fixtures/bun/bun.lock create mode 100644 .dagger/modules/runtimes/fixtures/bun/dagger-module.toml create mode 100644 .dagger/modules/runtimes/fixtures/bun/package.json create mode 100644 .dagger/modules/runtimes/fixtures/bun/src/index.ts create mode 100644 .dagger/modules/runtimes/fixtures/deno/.gitignore create mode 100644 .dagger/modules/runtimes/fixtures/deno/dagger-module.toml create mode 100644 .dagger/modules/runtimes/fixtures/deno/deno.json create mode 100644 .dagger/modules/runtimes/fixtures/deno/src/index.ts create mode 100644 .dagger/modules/runtimes/fixtures/node/.gitignore create mode 100644 .dagger/modules/runtimes/fixtures/node/dagger-module.toml create mode 100644 .dagger/modules/runtimes/fixtures/node/package.json create mode 100644 .dagger/modules/runtimes/fixtures/node/src/index.ts create mode 100644 .dagger/modules/runtimes/main.dang diff --git a/.dagger/modules/runtimes/dagger.json b/.dagger/modules/runtimes/dagger.json new file mode 100644 index 0000000..a226edf --- /dev/null +++ b/.dagger/modules/runtimes/dagger.json @@ -0,0 +1,13 @@ +{ + "name": "runtimes", + "engineVersion": "v1.0.0-0", + "sdk": { + "source": "dang" + }, + "dependencies": [ + { + "name": "typescript-sdk", + "source": "../../../" + } + ] +} diff --git a/.dagger/modules/runtimes/fixtures/bun/.gitignore b/.dagger/modules/runtimes/fixtures/bun/.gitignore new file mode 100644 index 0000000..621ed55 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/bun/.gitignore @@ -0,0 +1,2 @@ +/**/node_modules/** +/.env diff --git a/.dagger/modules/runtimes/fixtures/bun/bun.lock b/.dagger/modules/runtimes/fixtures/bun/bun.lock new file mode 100644 index 0000000..e69de29 diff --git a/.dagger/modules/runtimes/fixtures/bun/dagger-module.toml b/.dagger/modules/runtimes/fixtures/bun/dagger-module.toml new file mode 100644 index 0000000..6448a29 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/bun/dagger-module.toml @@ -0,0 +1,5 @@ +name = "runtime-bun" +engineVersion = "v1.0.0-beta.9" + +[runtime] + source = "typescript" diff --git a/.dagger/modules/runtimes/fixtures/bun/package.json b/.dagger/modules/runtimes/fixtures/bun/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/bun/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/.dagger/modules/runtimes/fixtures/bun/src/index.ts b/.dagger/modules/runtimes/fixtures/bun/src/index.ts new file mode 100644 index 0000000..e3e7fc4 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/bun/src/index.ts @@ -0,0 +1,9 @@ +import { object, func } from "@dagger.io/dagger" + +@object() +export class RuntimeBun { + @func() + greet(name: string): string { + return "hello " + name + " from bun" + } +} diff --git a/.dagger/modules/runtimes/fixtures/deno/.gitignore b/.dagger/modules/runtimes/fixtures/deno/.gitignore new file mode 100644 index 0000000..621ed55 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/deno/.gitignore @@ -0,0 +1,2 @@ +/**/node_modules/** +/.env diff --git a/.dagger/modules/runtimes/fixtures/deno/dagger-module.toml b/.dagger/modules/runtimes/fixtures/deno/dagger-module.toml new file mode 100644 index 0000000..66e89a1 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/deno/dagger-module.toml @@ -0,0 +1,5 @@ +name = "runtime-deno" +engineVersion = "v1.0.0-beta.9" + +[runtime] + source = "typescript" diff --git a/.dagger/modules/runtimes/fixtures/deno/deno.json b/.dagger/modules/runtimes/fixtures/deno/deno.json new file mode 100644 index 0000000..36e1765 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/deno/deno.json @@ -0,0 +1,5 @@ +{ + "tasks": { + "dev": "deno run main.ts" + } +} diff --git a/.dagger/modules/runtimes/fixtures/deno/src/index.ts b/.dagger/modules/runtimes/fixtures/deno/src/index.ts new file mode 100644 index 0000000..64c3e92 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/deno/src/index.ts @@ -0,0 +1,9 @@ +import { object, func } from "@dagger.io/dagger" + +@object() +export class RuntimeDeno { + @func() + greet(name: string): string { + return "hello " + name + " from deno" + } +} diff --git a/.dagger/modules/runtimes/fixtures/node/.gitignore b/.dagger/modules/runtimes/fixtures/node/.gitignore new file mode 100644 index 0000000..621ed55 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/node/.gitignore @@ -0,0 +1,2 @@ +/**/node_modules/** +/.env diff --git a/.dagger/modules/runtimes/fixtures/node/dagger-module.toml b/.dagger/modules/runtimes/fixtures/node/dagger-module.toml new file mode 100644 index 0000000..ab4c158 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/node/dagger-module.toml @@ -0,0 +1,5 @@ +name = "runtime-node" +engineVersion = "v1.0.0-beta.9" + +[runtime] + source = "typescript" diff --git a/.dagger/modules/runtimes/fixtures/node/package.json b/.dagger/modules/runtimes/fixtures/node/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/node/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/.dagger/modules/runtimes/fixtures/node/src/index.ts b/.dagger/modules/runtimes/fixtures/node/src/index.ts new file mode 100644 index 0000000..37fcba5 --- /dev/null +++ b/.dagger/modules/runtimes/fixtures/node/src/index.ts @@ -0,0 +1,9 @@ +import { object, func } from "@dagger.io/dagger" + +@object() +export class RuntimeNode { + @func() + greet(name: string): string { + return "hello " + name + " from node" + } +} diff --git a/.dagger/modules/runtimes/main.dang b/.dagger/modules/runtimes/main.dang new file mode 100644 index 0000000..f8f0745 --- /dev/null +++ b/.dagger/modules/runtimes/main.dang @@ -0,0 +1,77 @@ +""" +Execution checks: does a module this SDK generates actually run? + +Every other check in this repo inspects what generation *writes*. These load +the result, which for a dagger-module.toml module means the engine builds the +runtime container and executes the generated entrypoint to discover its types — +there is no codegen at call time to paper over a bad tree. + +That makes them the only checks covering the contract the whole move rests on: +sdk/ resolving as @dagger.io/dagger, the bundle it holds being loadable, the +entrypoint importing the user's classes and registering them. A break here is a +module that generates cleanly and then fails at `dagger call`. + +One fixture per TypeScript runtime, because this is where they stop being +interchangeable: generation differs only in which config file it writes, but +node, bun and deno each run the entrypoint in a different container, with a +different interpreter, resolving @dagger.io/dagger a different way. +""" +type Runtimes { + let fixtures: String! = ".dagger/modules/runtimes/fixtures" + + """ + A node module should generate and run. + """ + nodeRunsCheck(ws: Workspace!): Void @check { + assertRuns(ws, "node", "RuntimeNode") + } + + """ + A bun module should generate and run. + + The engine picks bun over node by finding a lockfile next to package.json, so + the generated tree is byte-identical to the node one — only the container it + is executed in differs. Nothing else in this repo has ever run bun. + """ + bunRunsCheck(ws: Workspace!): Void @check { + assertRuns(ws, "bun", "RuntimeBun") + } + + """ + A deno module should generate and run. + + Deno has no node_modules to fall back on: if generation writes the wrong + config, the runtime cannot resolve @dagger.io/dagger at all. + """ + denoRunsCheck(ws: Workspace!): Void @check { + assertRuns(ws, "deno", "RuntimeDeno") + } + + """ + Generate a fixture, stage the result, and load the module from it. + + Staging through withChanges rather than writing to disk keeps the generated + tree out of the fixture entirely — which also sidesteps the trap that a + module whose .gitignore covers sdk/ hides its own generated files from the + engine's module context. + + Reading the module's objects is what forces execution: the TypeScript SDK has + no ModuleTypes function, so type discovery goes through the runtime and the + committed entrypoint's register(). + """ + let assertRuns(ws: Workspace!, runtime: String!, objectName: String!): Void { + let path = fixtures + "/" + runtime + let staged = ws.withChanges(typescriptSdk.mod(ws, path: path).generate(ws)) + + let objects = staged.moduleSource("/" + path).asModule.objects + .{{ asObject.{{ name }} }} + .map { o => o.asObject.name } + + if (objects.filter { name => name == objectName }.length == 0) { + raise runtime + " module did not load: expected object " + objectName + + ", got [" + objects.join(", ") + "]" + } + + null + } +} diff --git a/dagger.toml b/dagger.toml index 7924f2a..55fd360 100644 --- a/dagger.toml +++ b/dagger.toml @@ -12,6 +12,9 @@ source = ".dagger/modules/e2e" [modules.packager] source = ".dagger/modules/packager" +[modules.runtimes] +source = ".dagger/modules/runtimes" + [modules.typescript-sdk] source = "." check.skip = ["*"] From 33da82f0c120a34ab5a20ffb1fd7f7b2b94245f9 Mon Sep 17 00:00:00 2001 From: Vasek - Tom C Date: Wed, 19 Aug 2026 18:17:42 +0200 Subject: [PATCH 2/2] test(runtimes): call a function on a scaffolded module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loading a module proves the entrypoint registers its types. This proves the other half of the dispatcher: that a call reaches the user's code and its return value comes back. sdk-sdk's harness already drives a release CLI through the whole user path — `sdk install`, `module init`, `generate` — so the call is one more command on the state it leaves behind. What its harness cannot do is init with a `--runtime` flag: initArgs is private and fixed, and a run's container is not exposed, so the per-runtime checks still build their own module. A seam there would be worth proposing upstream. The function is the default template's baseImageAddress, whose value the module's constructor sets, so a passing call also means the constructor ran and its default was applied. Confirmed by changing that default and watching the check report the new value. Signed-off-by: Tom Chauveau --- .dagger/modules/e2e/util.dang | 12 ++++++++- .dagger/modules/runtimes/dagger.json | 4 +++ .../modules/runtimes/fixtures/bun/.gitignore | 3 +++ .../modules/runtimes/fixtures/deno/.gitignore | 3 +++ .../modules/runtimes/fixtures/deno/deno.json | 15 +++++++++++ .../modules/runtimes/fixtures/node/.gitignore | 3 +++ .dagger/modules/runtimes/main.dang | 26 +++++++++++++++++++ dagger.toml | 9 +++++++ 8 files changed, 74 insertions(+), 1 deletion(-) diff --git a/.dagger/modules/e2e/util.dang b/.dagger/modules/e2e/util.dang index af705b5..64585e9 100644 --- a/.dagger/modules/e2e/util.dang +++ b/.dagger/modules/e2e/util.dang @@ -152,6 +152,16 @@ type Fixtures { that depends on the runtime, deno.json instead of package.json + tsconfig. """ let denoModule: String! = root + "/generate-deno/app" + + """ + The runtime execution fixtures, managed so `dagger generate` keeps them + current, and registered by the engine when the runtimes module loads them. + """ + let runtimeFixtures: [String!]! = [ + ".dagger/modules/runtimes/fixtures/bun", + ".dagger/modules/runtimes/fixtures/deno", + ".dagger/modules/runtimes/fixtures/node", + ] let configBareModule: String! = root + "/config/bare" # A client binds to client/app (which depends on client/dep) and is generated @@ -193,5 +203,5 @@ type Fixtures { depLibModule, denoModule, parentSourceModule, - ] + ] + runtimeFixtures } diff --git a/.dagger/modules/runtimes/dagger.json b/.dagger/modules/runtimes/dagger.json index a226edf..1962b85 100644 --- a/.dagger/modules/runtimes/dagger.json +++ b/.dagger/modules/runtimes/dagger.json @@ -8,6 +8,10 @@ { "name": "typescript-sdk", "source": "../../../" + }, + { + "name": "sdk-sdk", + "source": "github.com/dagger/sdk-sdk" } ] } diff --git a/.dagger/modules/runtimes/fixtures/bun/.gitignore b/.dagger/modules/runtimes/fixtures/bun/.gitignore index 621ed55..5b4e8f0 100644 --- a/.dagger/modules/runtimes/fixtures/bun/.gitignore +++ b/.dagger/modules/runtimes/fixtures/bun/.gitignore @@ -1,2 +1,5 @@ +/sdk +/__dagger.entrypoint.ts +/tsconfig.json /**/node_modules/** /.env diff --git a/.dagger/modules/runtimes/fixtures/deno/.gitignore b/.dagger/modules/runtimes/fixtures/deno/.gitignore index 621ed55..5b4e8f0 100644 --- a/.dagger/modules/runtimes/fixtures/deno/.gitignore +++ b/.dagger/modules/runtimes/fixtures/deno/.gitignore @@ -1,2 +1,5 @@ +/sdk +/__dagger.entrypoint.ts +/tsconfig.json /**/node_modules/** /.env diff --git a/.dagger/modules/runtimes/fixtures/deno/deno.json b/.dagger/modules/runtimes/fixtures/deno/deno.json index 36e1765..256151a 100644 --- a/.dagger/modules/runtimes/fixtures/deno/deno.json +++ b/.dagger/modules/runtimes/fixtures/deno/deno.json @@ -1,5 +1,20 @@ { "tasks": { "dev": "deno run main.ts" + }, + "imports": { + "typescript": "npm:typescript@5.9.3", + "@dagger.io/dagger": "./sdk/index.ts", + "@dagger.io/dagger/telemetry": "./sdk/telemetry.ts" + }, + "nodeModulesDir": "auto", + "unstable": [ + "bare-node-builtins", + "sloppy-imports", + "node-globals", + "byonm" + ], + "compilerOptions": { + "experimentalDecorators": true } } diff --git a/.dagger/modules/runtimes/fixtures/node/.gitignore b/.dagger/modules/runtimes/fixtures/node/.gitignore index 621ed55..5b4e8f0 100644 --- a/.dagger/modules/runtimes/fixtures/node/.gitignore +++ b/.dagger/modules/runtimes/fixtures/node/.gitignore @@ -1,2 +1,5 @@ +/sdk +/__dagger.entrypoint.ts +/tsconfig.json /**/node_modules/** /.env diff --git a/.dagger/modules/runtimes/main.dang b/.dagger/modules/runtimes/main.dang index f8f0745..df7c929 100644 --- a/.dagger/modules/runtimes/main.dang +++ b/.dagger/modules/runtimes/main.dang @@ -47,6 +47,32 @@ type Runtimes { assertRuns(ws, "deno", "RuntimeDeno") } + """ + A scaffolded module should answer a function call. + + Loading a module proves the entrypoint registers its types; this proves the + other half of the dispatcher — that a call reaches the user's code and its + return value comes back. It goes through sdk-sdk's harness, which builds a + scratch workspace and drives a release CLI the way a user would: `sdk + install`, `module init`, `generate`, then the call. + + The function is the default template's `baseImageAddress`, whose value is set + by the module's constructor, so a passing call also means the constructor ran + and its default was applied. + """ + invokesFunctionCheck(ws: Workspace!): Void @check { + let target = sdkSdk.target(ws.directory("/"), ".") + let run = target.run(["call", target.moduleName, "base-image-address"]) + + run.assertSuccess + + if (run.stdout.contains("alpine") == false) { + raise "calling a scaffolded module returned unexpected output: " + run.stdout + } + + null + } + """ Generate a fixture, stage the result, and load the module from it. diff --git a/dagger.toml b/dagger.toml index 55fd360..1a39d26 100644 --- a/dagger.toml +++ b/dagger.toml @@ -54,6 +54,15 @@ path = ".dagger/modules/e2e/fixtures/generate-deps/dep" [[modules.typescript-sdk.as-sdk.modules]] path = ".dagger/modules/e2e/fixtures/parent-source/.dagger/modules/app" +[[modules.typescript-sdk.as-sdk.modules]] +path = ".dagger/modules/runtimes/fixtures/bun" + +[[modules.typescript-sdk.as-sdk.modules]] +path = ".dagger/modules/runtimes/fixtures/deno" + +[[modules.typescript-sdk.as-sdk.modules]] +path = ".dagger/modules/runtimes/fixtures/node" + [[modules.typescript-sdk.as-sdk.clients]] path = ".dagger/modules/e2e/fixtures/client/out" module = ".dagger/modules/e2e/fixtures/client/app"