Ctrl K
ComunidadGuías…

roadmap.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
You already have the first primitive of the execution engine working:

upload binary
    ↓
store in MinIO
    ↓
create Test record
    ↓
download binary
    ↓
execute
    ↓
capture output + exit code
    ↓
create TestRun
    ↓
store logs

That's a legitimate starting point for BYOT.

The important thing now is not to throw this away. I'd evolve it into the architecture we discussed.
What you have today

Your current flow is essentially:

HTTP request
     │
     ▼
Run()
     │
     ├── Get Test
     │
     ├── Download binary
     │
     ├── /tmp/test
     │
     ├── runtime.GetRuntime()
     │
     ├── rt.Run()
     │
     ├── collect output
     │
     ├── upload logs
     │
     └── create TestRun

And that's actually a nice separation already:

rt := runtime.GetRuntime(testRes.Runtime)

output, exitCode, err :=
    rt.Run(testRes.UUID, testRes.TimeoutSeconds)

That runtime abstraction is the thing I'd preserve.
The biggest problem right now

Your HTTP handler is currently doing the execution itself.

POST /run
   │
   ▼
HTTP handler
   │
   ▼
download
   │
   ▼
execute
   │
   ▼
wait 30/60/120 seconds
   │
   ▼
respond

That will become painful very quickly.

Imagine 100 tests scheduled around the same minute.

You don't want:

100 HTTP requests
     ↓
100 processes
     ↓
100 containers

inside your API server.

Instead:

POST /run
    │
    ▼
Create Run
    │
    ▼
Queue Job
    │
    ▼
HTTP 202

Then:

                Queue
                  │
       ┌──────────┼──────────┐
       ▼          ▼          ▼
    Worker      Worker     Worker
       │          │          │
       ▼          ▼          ▼
    Runtime     Runtime     Runtime

That's the first major architectural change I'd make.
Your runtime abstraction is exactly the right direction

I'd take what you already have and formalize it.

Something like:

type Runtime interface {
    Prepare(ctx context.Context, test Test) error
    Run(ctx context.Context, test Test) (*Result, error)
    Cleanup(ctx context.Context, test Test) error
}

Then:

Runtime
   │
   ├── GoRuntime
   ├── NodeRuntime
   ├── PlaywrightRuntime
   └── K6Runtime

But here's an important distinction.

Your current:

Go → compile binary → execute binary

is perfectly fine for your current experiment.

I wouldn't make “everything must become a binary” the permanent architecture.
Why?

Playwright is going to expose this immediately.

A Playwright project isn't naturally:

checkout.bin

It's more like:

checkout/
├── package.json
├── package-lock.json
├── playwright.config.ts
├── tests/
│   └── checkout.spec.ts
└── ...

And it needs:

Node
Playwright
Chromium

So eventually your test artifact should be a package, not necessarily a binary.

I'd change the conceptual model from:

Test
 └── Binary

to:

Test
 └── Artifact
      ├── type
      ├── runtime
      ├── command
      └── storage location

Then:

Go
    artifact → binary

Playwright
    artifact → project bundle

k6
    artifact → JS bundle

Python
    artifact → project/package

Much more flexible.
Your current Test model should evolve

Right now you're effectively doing:

Test
├── UUID
├── OriginalFilename
├── Runtime
├── Severity
├── BinaryURL
└── TimeoutSeconds

I'd move toward something like:

type Test struct {
    ID          string
    Name        string

    Runtime     string
    Command     string

    ArtifactKey string

    Timeout     time.Duration

    Schedule    *string

    CreatedAt   time.Time
}

For example:

{
  "name": "checkout",
  "runtime": "playwright",
  "command": "npx playwright test",
  "artifact": "tests/abc123",
  "timeout": 120
}

Or:

{
  "name": "api-health",
  "runtime": "k6",
  "command": "k6 run script.js",
  "artifact": "tests/xyz789",
  "timeout": 60
}

The command is particularly important.

It means your engine doesn't need to understand every possible test framework.

It mostly needs to provide the environment in which the command runs.
Then your runtime becomes an environment builder

This is the architecture I'd aim for:

                    Test
                     │
                     ▼
              Execution Request
                     │
                     ▼
              Runtime Resolver
                     │
        ┌────────────┼────────────┐
        ▼            ▼            ▼
   Playwright       k6           Go
        │            │            │
        ▼            ▼            ▼
   Environment   Environment   Environment
        │            │            │
        └────────────┼────────────┘
                     ▼
                  Runner
                     │
                     ▼
                 Sandbox
                     │
                     ▼
                Command
                     │
                     ▼
                  Result

That's a much stronger abstraction.
The next thing I'd build: Runner

Right now:

rt.Run(...)

is doing too much conceptually.

I'd introduce:

type Runner interface {
    Run(ctx context.Context, req ExecutionRequest) (*ExecutionResult, error)
}

Where:

type ExecutionRequest struct {
    RunID       string
    TestID      string
    Runtime     string
    ArtifactKey string
    Command     string
    Timeout     time.Duration
}

And:

type ExecutionResult struct {
    ExitCode  int
    Status    string
    Duration  time.Duration

    Stdout    []byte
    Stderr    []byte

    Artifacts []Artifact
}

Now your API doesn't care how execution happens.
And this is where Docker enters

Your current architecture is effectively:

API process
   │
   └── execute customer binary

I'd move toward:

Worker
   │
   ▼
Docker container
   │
   ├── test artifact
   ├── runtime
   ├── environment
   ├── secrets
   └── command
   │
   ▼
execution

So your Go worker might eventually do something conceptually like:

result, err := runner.Run(ctx, ExecutionRequest{
    RunID:       run.ID,
    Runtime:     test.Runtime,
    ArtifactKey: test.ArtifactKey,
    Command:     test.Command,
    Timeout:     test.Timeout,
})

The runner creates the sandbox.
There's also a subtle bug/security issue in your current code

You're doing:

tmpPath := "/tmp/" + testRes.UUID

That's okay-ish because UUID is controlled by your system, but I'd still avoid manually constructing execution paths like this.

More importantly:

os.Chmod(tmpPath, 0755)

followed by executing the customer's uploaded binary means your API host is executing customer-controlled code directly.

That is the biggest thing I'd change before exposing this to real external users.

For an internal prototype:

    Fine.

For a SaaS:

    Absolutely not.

The customer code should execute inside an isolated sandbox.
Your MinIO choice is actually good

I'd keep this.

You already have:

MinIO
  │
  ├── test artifacts
  └── logs

Eventually:

bucket/
├── tests/
│   └── test-id/
│       └── version-id/
│
└── runs/
    └── run-id/
        ├── stdout.log
        ├── stderr.log
        ├── screenshot.png
        ├── trace.zip
        └── result.json

That's a nice storage model.
One thing I'd change about logs

Right now:

output, exitCode, err := rt.Run(...)

means the entire output lives in memory.

That's okay for your tiny test.

But eventually a test can produce:

10 MB
100 MB
500 MB

You don't want:

[]byte

holding an enormous test output inside the worker.

I'd move toward streaming:

process
   │
   ├── stdout ──────┐
   │                │
   └── stderr ──────┤
                    ▼
              Log collector
                    │
                    ▼
                 MinIO

Then keep only a small tail/summary in the database.
Your TestRun is already heading in the right direction

You have:

TestRun{
    UUID
    TestID
    StartedAt
    DurationMs
    LogURL
    FinishedAt
    Status
    LogSizeBytes
}

I'd eventually add:

Run
├── id
├── test_id
├── status
├── queued_at
├── started_at
├── finished_at
├── duration_ms
├── exit_code
├── error_code
├── worker_id
├── region
├── runtime_version
└── ...

And separately:

RunArtifact
├── run_id
├── type
├── storage_key
└── size

That will support Playwright beautifully later.
Your architecture today vs where I'd take it
Today

                HTTP
                 │
                 ▼
              Handler
                 │
       ┌─────────┼─────────┐
       ▼         ▼         ▼
    Storage    Runtime    DB
                 │
                 ▼
             execute

Next version

                HTTP
                 │
                 ▼
               API
                 │
                 ▼
              Postgres
                 │
                 ▼
                Queue
                 │
                 ▼
               Worker
                 │
                 ▼
              Runner
                 │
                 ▼
             Sandbox
                 │
                 ▼
              Runtime
                 │
                 ▼
              Result
             /      \
            ▼        ▼
         MinIO    Postgres
            │
            ▼
         Artifacts

Eventually

                         BYOT
                          │
              ┌───────────┴───────────┐
              │                       │
          Control Plane          Execution Plane
              │                       │
       API / Scheduler              Queue
       Tests / Alerts                 │
              │             ┌─────────┼─────────┐
              │             ▼         ▼         ▼
              │          Mumbai   Singapore  Frankfurt
              │             │         │         │
              │          Sandbox   Sandbox   Sandbox
              │             │         │         │
              └─────────────┴─────────┴─────────┘
                            │
                         Results
                            │
                 ┌──────────┴──────────┐
                 ▼                     ▼
             Postgres                MinIO
                 │                     │
                 └──────────┬──────────┘
                            ▼
                       Alert Engine

And this is why I like what you've already built

You haven't built the wrong thing.

You've already discovered the core loop:

Artifact
   ↓
Runtime
   ↓
Execute
   ↓
Exit code
   ↓
Logs
   ↓
TestRun

That's the kernel of BYOT.

I'd keep that kernel and build outward rather than rewriting it.

The next three engineering steps I'd personally take are:

    Move execution out of the HTTP handler → queue + worker.

    Put execution inside an isolated container/sandbox.

    Change “binary” into a generic artifact + command model so Playwright/k6 fit naturally.

After those three, I'd implement Playwright as the first real runtime. That's where your current architecture will get stress-tested in a very useful way.