Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eb5a904
functions testing harness with simple-bash. hello-world, and runtime-…
Jovonni Jan 8, 2026
ce3bce3
various function prototypes, working on common function contract
Jovonni Jan 8, 2026
6559bdf
added cicd
Jovonni Jan 9, 2026
0ad374c
updated various functions, with global runner
Jovonni Jan 9, 2026
7177a64
twilio, and using new envvars
Jovonni Jan 9, 2026
23d8ae0
added new env vars to the test-runner.ts, can pass all (for now, TODO)
Jovonni Jan 9, 2026
ba1f218
dotenv dep for .env
Jovonni Jan 9, 2026
c07e4ef
updated several functions
Jovonni Jan 9, 2026
f6a363f
makefile changes and scripts and lock
Jovonni Jan 9, 2026
874a38e
chore: ignore opencode binary artifact
Jovonni Jan 9, 2026
4c136e4
Merge pull request #4 from constructive-io/d/function-testing-foundation
Jovonni Jan 10, 2026
531d0a2
warn on gql fallback
Jovonni Jan 10, 2026
c8bca84
added matrix
Jovonni Jan 10, 2026
219dc23
calvin api a envvar
Jovonni Jan 10, 2026
d4163e9
Merge pull request #6 from constructive-io/dev/testing-strategy
Jovonni Jan 10, 2026
0a6b034
feat: add pgpm-dump function and standardize k8s test runner to v4
Jovonni Jan 11, 2026
dfce124
fix(ci): make kind binary path resolution dynamic in Makefile
Jovonni Jan 11, 2026
ecd7c5d
fix(ci): parameterize KIND_CLUSTER_NAME to support CI 'local' cluster
Jovonni Jan 11, 2026
c34dc03
fix(ci): remove unconfigured submodule _calvincode_build from git index
Jovonni Jan 11, 2026
22aa874
Merge pull request #5 from constructive-io/dev/various-functions-2
Jovonni Jan 11, 2026
6fff7e7
feat: add sql-schema-transform cloud function
pyramation Jan 11, 2026
dd2c7e9
chore: update pnpm-lock.yaml
pyramation Jan 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
packages
dist
.git
.env
41 changes: 41 additions & 0 deletions .github/workflows/test-k8s-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ jobs:
k8s-ci-test:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
function:
- hello-world
- llm-internal-calvin
- opencode-headless
- twilio-sms
- llm-external
- send-email-link
- crypto-login
- github-repo-creator
- pytorch-gpu
- runtime-script
- rust-hello-world
- simple-bash
- simple-email
- stripe-function

steps:
- name: Checkout
Expand Down Expand Up @@ -196,6 +214,29 @@ jobs:
echo "All pods (final):" && kubectl get pods -A
echo "Knative services:" && kubectl get ksvc -A || true

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9

- name: Install dependencies
run: pnpm install --no-frozen-lockfile

- name: Build and Load Test Runner Image
run: |
make build-test-runner KIND_CLUSTER_NAME=local

- name: Run K8s Tests
run: |
# Ensure kubectl proxy port is available or managed by the runner
pnpm exec ts-node scripts/test-runner.ts --function ${{ matrix.function }}


- name: Dump diagnostics on failure
if: always()
run: |
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,5 @@ dist
# Vite logs files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
functions/opencode-headless/_calvincode_build
functions/opencode-headless/bin/
56 changes: 52 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
.PHONY: build clean lint docker-build docker-build-simple-email docker-build-send-email-link docker-push docker-push-simple-email docker-push-send-email-link
.PHONY: build clean lint test test-all build-test-runner docker-build docker-build-simple-email docker-build-send-email-link docker-push docker-push-simple-email docker-push-send-email-link

REGISTRY := ghcr.io/constructive-io/constructive-functions
# Detect kind binary (search PATH, fallback to Homebrew)
KIND_BIN := $(shell which kind)
ifeq ($(KIND_BIN),)
KIND_BIN := /opt/homebrew/bin/kind
endif
KIND_CLUSTER_NAME ?= interweb-local

SUBDIRS := functions/hello-world functions/simple-email functions/send-email-link functions/runtime-script

build:
pnpm run build
pnpm -r build

clean:
pnpm run clean
pnpm -r clean

lint:
pnpm run lint
pnpm -r lint

test:
pnpm -r test

# Docker Build & Push (Restored)
docker-build:
@echo "Building Docker images for functions..."
@for fn in functions/*; do \
Expand Down Expand Up @@ -40,3 +52,39 @@ docker-push-simple-email:

docker-push-send-email-link:
docker push $(REGISTRY)/send-email-link:latest

# Kubernetes Test Runner
# Run All Tests inside K8s (Centralized Runner)
test-k8s-all:
@echo "Running all K8s tests via centralized KubernetesJS runner..."
pnpm exec ts-node scripts/test-runner.ts

build-test-runner:
@echo "Building Shared Test Runner Image..."
docker build -f functions/_runtimes/node/Dockerfile.test -t constructive/function-test-runner:v4 .
$(KIND_BIN) load docker-image constructive/function-test-runner:v4 --name $(KIND_CLUSTER_NAME)

# Individual Test Shortcuts
test-calvin:
pnpm exec ts-node scripts/test-runner.ts --function llm-internal-calvin

test-opencode-headless:
pnpm exec ts-node scripts/test-runner.ts --function opencode-headless

test-twilio:
pnpm exec ts-node scripts/test-runner.ts --function twilio-sms

test-llm-external:
pnpm exec ts-node scripts/test-runner.ts --function llm-external

test-email:
pnpm exec ts-node scripts/test-runner.ts --function send-email-link

# Cleanup K8s Resources
k8s-clean:
@echo "Cleaning up K8s jobs for constructive-functions..."
# Delete all jobs matching test-* or *-exec-* pattern (batch delete)
@kubectl get jobs -n default --no-headers -o custom-columns=":metadata.name" | grep -E "^test-|-exec-" | xargs kubectl delete job -n default --ignore-not-found || true
# Delete all pods matching test-* or *-exec-* pattern (orphaned pods) (batch delete)
@kubectl get pods -n default --no-headers -o custom-columns=":metadata.name" | grep -E "^test-|-exec-" | xargs kubectl delete pod -n default --ignore-not-found || true
@echo "Done."
74 changes: 74 additions & 0 deletions functions/_runtimes/agentic/Dockerfile.agentic
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Python runtime for LLM API inference (OpenAI & Claude)
# This module makes API calls to OpenAI and Anthropic for LLM inference
# For the full fat container with Ollama & local models, see Dockerfile.ollama
# Based on: /Users/0xj0/Documents/projects/LQL/CONSTRUCTIVE/agentic-foundation

##################### heres what is had inside of (/Users/0xj0/Documents/projects/LQL/CONSTRUCTIVE/agentic-foundation) -- GO VERIFY YOURSELF

# Builder Stage
FROM rust:latest as builder
WORKDIR /app
COPY . .
# Build agent_core
RUN cargo build --release --bin agent_core

# Runtime Stage - "Fat Container"
FROM ubuntu:22.04
WORKDIR /app

# Set non-interactive install
ENV DEBIAN_FRONTEND=noninteractive

# 1. Install Basic Tools & Runtimes (Python, Node, System Utils)
RUN apt-get update && apt-get install -y \
curl wget git build-essential \
python3 python3-pip python3-venv \
nodejs npm \
postgresql-14 postgresql-client-14 \
sudo \
libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 \
chromium-browser \
&& rm -rf /var/lib/apt/lists/*

# 2. Install Rust in Runtime (for the agent to use `cargo`)
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"

# 3. Install PostGraphile
# 3. Install PostGraphile
RUN npm install -g pnpm && pnpm add -g postgraphile @graphile-contrib/pg-simplify-inflector

# 4. Install Ollama & Bake Models
# We install Ollama, then start it in the background to pull models into the image layers.
RUN curl -fsSL https://ollama.com/install.sh | sh

# Pre-pull Models (Using available equivalents for the '2025' spec models)
# GPT-OSS -> llama3.2 (Small, open, robust)
# Qwen3-VL -> llava (Vision model standard in Ollama)
# Devstral -> qwen2.5-coder (Excellent coding model)
# Nemotron -> mistral (Strong reasoning)
RUN nohup bash -c "ollama serve" & \
sleep 10 && \
ollama pull llama3.2 && \
ollama pull llava && \
ollama pull qwen2.5-coder && \
ollama pull mistral && \
pkill ollama

# 5. Setup Data & Permissions
RUN mkdir -p /var/lib/postgresql/data && chown -R postgres:postgres /var/lib/postgresql/data

# 6. Copy Binaries & Scripts
COPY --from=builder /app/target/release/agent_core /app/agent_core
COPY scripts/entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

# 7. Config
ENV DATABASE_URL=postgres://agent:agent@localhost:5432/agentic
ENV OLLAMA_HOST=0.0.0.0:11434

EXPOSE 3000 5432 11434 5000

ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["./agent_core"]
18 changes: 18 additions & 0 deletions functions/_runtimes/node/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM node:22-alpine

WORKDIR /usr/src/app

COPY package.json ./

RUN npm install -g pnpm@10.12.2 && pnpm install --prod

COPY dist ./dist
COPY runner.js ./runner.js

ENV NODE_ENV=production
ENV PORT=8080

USER node

CMD ["node", "runner.js", "dist/index.js"]

44 changes: 44 additions & 0 deletions functions/_runtimes/node/Dockerfile.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
FROM node:20-alpine

# Install Postgres Client and Build Tools
RUN apk add --no-cache postgresql-client bash make g++ python3 kubectl

COPY . /app
WORKDIR /app

# Ensure clean slate
RUN rm -rf node_modules

# 3. Configure PNPM Home
ENV PNPM_HOME="/root/.local/share/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ENV SHELL="/bin/bash"

# 4. Install Dependencies from NPM
RUN npm install -g pnpm@9 && \
pnpm setup && \
pnpm install --no-frozen-lockfile

# 5. Connect to global (not needed for pnpm v9+)
# RUN pnpm route-global

# 6. Install PGPM from NPM
RUN pnpm add -g pgpm

# Run as postgres user to avoid 'role root does not exist' in pgsql-test
# handle existing user/group if created by apk
RUN (getent group postgres || addgroup -S postgres) && (getent passwd postgres || adduser -S postgres -G postgres)
RUN chown -R postgres:postgres /app
USER postgres
ENV USER=postgres
ENV PGUSER=postgres

# 6. Setup Entrypoint (Inlined for Minimalism)
ENV NODE_ENV=test
CMD ["/bin/bash", "-c", "set -e; \
echo \"Waiting for Postgres at $PGHOST:$PGPORT...\"; \
until pg_isready -h \"$PGHOST\" -p \"$PGPORT\" -U \"$PGUSER\"; do echo \"Waiting...\"; sleep 2; done; \
echo \"Deploying Schema...\"; \
pgpm deploy --package pgpm-database-jobs --database template1 --yes 2>/dev/null || echo \"Deploy continued...\"; \
unset PGDATABASE; \
pnpm test"]
118 changes: 118 additions & 0 deletions functions/_runtimes/node/runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const path = require('path');
const fs = require('fs');

const run = async () => {
// 1. Resolve Dependencies from CWD (User's Function Context)
// This logic ensures we find express/graphql-request in the function's node_modules,
// regardless of where runner.js is located (Local Dev vs Docker).
const resolveDep = (name) => {
try {
return require(require.resolve(name, { paths: [process.cwd()] }));
} catch (e) {
console.error(`[runner] Failed to resolve dependency '${name}' from ${process.cwd()}`);
console.error(e.message);
process.exit(1);
}
};

const express = resolveDep('express');
const bodyParser = resolveDep('body-parser');
const { GraphQLClient } = resolveDep('graphql-request');
const http = require('http');
const https = require('https');
const { URL } = require('url');

// 2. Resolve User Handler
const relativePath = process.argv[2] || 'dist/index.js';
const absolutePath = path.resolve(process.cwd(), relativePath);

let userModule;
try {
userModule = require(absolutePath);
} catch (e) {
console.error(`[runner] Failed to load function at ${absolutePath}`);
console.error(e.message);
process.exit(1);
}

const handler = userModule.default || userModule;

if (typeof handler !== 'function') {
console.error(`[runner] Export at ${absolutePath} is not a function.`);
process.exit(1);
}

// 3. Setup App & Helper Functions (Ported from knative-job-fn/src/index.ts)
// We implement a simplified version of the logic to avoid needing deep imports.
// However, since we are replacing the shim which used `express` directly usually,
// or `knative-job-fn` library...
// Correct approach: The shim used `app` from `@constructive-io/knative-job-fn`.
// We should try to use THAT if available, to preserve exact behavior (headers, logging).

let app;
try {
// Try to load the standard wrapper if present
const jobFn = resolveDep('@constructive-io/knative-job-fn');
// The library usually exports { default: { post: ..., listen: ... } } or similar?
// Let's check how functions imported it: "import app from '@constructive-io/knative-job-fn';"
// It exports 'default'.
const lib = jobFn.default || jobFn;

// The library exposes an 'app' like object but 'listen' is the main entry.
// But we want to inject our handler into a route.
// Library usage in shim: `app.post('/', ...)`
// Library implementation: `app` IS express() basically, but wrapped.

// Actually the library exports an object: { post: ..., listen: ... }
// We can use it directly.
app = lib;
} catch (e) {
// Fallback to raw express if wrapper missing (unlikely given package.json)
console.warn('[runner] @constructive-io/knative-job-fn not found, falling back to raw express');
app = express();
app.use(bodyParser.json());
}

// 4. Setup GraphQL Client
const graphqlEndpoint = process.env.GRAPHQL_ENDPOINT || 'http://constructive-server:3000/graphql';
if (!process.env.GRAPHQL_ENDPOINT) {
// Warn if falling back, to aid debugging
console.warn(`[runner] GRAPHQL_ENDPOINT not set, defaulting to internal k8s service: ${graphqlEndpoint}`);
}
const client = new GraphQLClient(graphqlEndpoint);

// 5. Setup Route
app.post('/', async (req, res) => {
try {
const result = await handler(req.body, { client, headers: req.headers });

// Standard Shim Error Handling Heuristics
if (result && result.error) {
// Heuristics for 400 vs 500
if (['Missing prompt', 'Unsupported provider', 'Missing "query" in payload',
'Missing repoName or githubToken', 'Missing X-Database-Id header or DEFAULT_DATABASE_ID',
'Missing required field', "Either 'html' or 'text' must be provided",
"Missing address, message, or signature"].some(s => result.error.includes(s) || s === result.error)) {
return res.status(400).json(result);
}
return res.status(500).json(result);
}

res.status(200).json(result);
} catch (e) {
console.error(e);
res.status(500).json({ error: e.message });
}
});

// 6. Start Server
const port = Number(process.env.PORT ?? 8080);
app.listen(port, () => {
console.log(`[runner] Function '${relativePath}' listening on port ${port}`);
});
};

run().catch(e => {
console.error('[runner] Fatal:', e);
process.exit(1);
});
Loading