Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 34 additions & 10 deletions .github/scripts/prod-invocation/create-test-app.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { execSync } from 'child_process';
import { execFileSync, execSync } from 'child_process';
import { readdirSync } from 'fs';
import { join } from 'path';

const TEST_APP_SOURCE_FILE_PATH = './integration-tests/test-application/test-app.js';
const TEST_APP_WASM_FILE_PATH = './integration-tests/test-application/test-app.wasm';
const TEST_APP_SOURCE_FILE_PATH = './integration-tests/test-application/test-app.ts';
const TEST_APP_WASM_FILE_PATH = './integration-tests/test-application/dist/test-app.wasm';
const TEST_APP_TSCONFIG_PATH = './integration-tests/test-application/tsconfig.json';
const CHECKS_SOURCE_DIR = './integration-tests/test-application/checks';
const CHECKS_DIST_DIR = './integration-tests/test-application/dist/checks';

export default async ({ github, context, core }) => {
// Ensure this is running in GitHub Actions
export default async ({ core }) => {
if (!process.env.GITHUB_ENV) {
throw new Error(
'GITHUB_ENV is not defined. This script must be run in a GitHub Actions environment.',
Expand All @@ -13,12 +17,9 @@ export default async ({ github, context, core }) => {

const workspaceDir = process.env.GITHUB_WORKSPACE || process.cwd();

// Build the test app code into a wasm binary
// Build the test app into a WASM binary
const buildResponse = execSync(
'./bin/fastedge-build.js --input ' +
TEST_APP_SOURCE_FILE_PATH +
' --output ' +
TEST_APP_WASM_FILE_PATH,
`./bin/fastedge-build.js --input ${TEST_APP_SOURCE_FILE_PATH} --output ${TEST_APP_WASM_FILE_PATH} --tsconfig ${TEST_APP_TSCONFIG_PATH}`,
{ encoding: 'utf8', cwd: workspaceDir },
);

Expand All @@ -29,4 +30,27 @@ export default async ({ github, context, core }) => {
}

core.info(`Test application built into wasm binary at ${TEST_APP_WASM_FILE_PATH}`);

// Compile TypeScript check modules to JS so invoke-test-app.js can import them in Node.js.
// Checks should import only routes.ts/types.ts, never handlers or fastedge:: modules. fastedge::
// is marked external as a guard: a stray handler/fastedge:: import in a check then surfaces when
// Node loads that check, rather than aborting the whole bundle at build time.
const checkFiles = readdirSync(join(workspaceDir, CHECKS_SOURCE_DIR))
.filter((f) => f.endsWith('.ts'))
.map((f) => join(CHECKS_SOURCE_DIR, f));

execFileSync(
'./node_modules/.bin/esbuild',
[
'--bundle',
'--format=esm',
'--platform=node',
'--external:fastedge::*',
`--outdir=${CHECKS_DIST_DIR}`,
...checkFiles,
],
{ encoding: 'utf8', cwd: workspaceDir },
);

core.info(`Check modules compiled to ${CHECKS_DIST_DIR}`);
};
85 changes: 23 additions & 62 deletions .github/scripts/prod-invocation/invoke-test-app.js
Original file line number Diff line number Diff line change
@@ -1,87 +1,48 @@
import { readdirSync } from 'fs';
import { join, resolve } from 'path';
import { pathToFileURL } from 'url';

const MAX_RETRIES = 3;
const INITIAL_WAIT_FOR_DEPLOYMENT_SECONDS = 15;
const RETRY_DELAY_SECONDS = 5;

const sleep = (secs) => new Promise((resolve) => setTimeout(resolve, secs * 1000));

async function checkEnv(appUrl, buildSha) {
const res = await fetch(appUrl);
if (res.status !== 200) throw new Error(`/: bad status ${res.status}`);
const data = await res.json();
if (data.build_sha !== buildSha) {
throw new Error(`/: build_sha mismatch: expected ${buildSha}, got ${data.build_sha}`);
}
}

async function checkOutboundFetch(appUrl) {
const res = await fetch(`${appUrl}/fetch`);
if (res.status !== 200) throw new Error(`/fetch: bad status ${res.status}`);
const data = await res.json();
if (!data.ok) {
throw new Error(`/fetch: outbound request failed (ok=${data.ok}, status=${data.status})`);
}
if (data.title !== 'Sample Slide Show') {
throw new Error(`/fetch: unexpected slideshow title: "${data.title}"`);
}
}

async function checkSecret(appUrl) {
const res = await fetch(`${appUrl}/secret`);
if (res.status !== 200) throw new Error(`/secret: bad status ${res.status}`);
const data = await res.json();
if (data.value !== 'hello-from-fastedge-secret') {
throw new Error(`/secret: wrong value "${data.value}"`);
}
}

async function checkRequestEcho(appUrl) {
const res = await fetch(`${appUrl}/echo`, {
method: 'POST',
headers: { 'x-test-header': 'hello-fastedge', 'content-type': 'text/plain' },
body: 'ping',
});
if (res.status !== 200) throw new Error(`/echo: bad status ${res.status}`);
const data = await res.json();
if (data.method !== 'POST') throw new Error(`/echo: wrong method "${data.method}"`);
if (data.body !== 'ping') throw new Error(`/echo: wrong body "${data.body}"`);
if (data.headers?.['x-test-header'] !== 'hello-fastedge') {
throw new Error(`/echo: wrong x-test-header "${data.headers?.['x-test-header']}"`);
}
}
const sleep = (secs) => new Promise((r) => setTimeout(r, secs * 1000));

export default async ({ github, context, core }) => {
export default async ({ context, core }) => {
if (!process.env.GITHUB_ENV) {
throw new Error(
'GITHUB_ENV is not defined. This script must be run in a GitHub Actions environment.',
);
}

const appUrl = process.env.APP_URL.replace(/\/$/, '');
const buildSha = context.sha;
const ctx = { buildSha: context.sha };

// Auto-discover compiled check modules from checks/dist/
const checksDir = resolve('./integration-tests/test-application/dist/checks');
const checkModules = await Promise.all(
readdirSync(checksDir)
.filter((f) => f.endsWith('.js'))
.map((f) => import(pathToFileURL(join(checksDir, f)).href)),
);

await sleep(INITIAL_WAIT_FOR_DEPLOYMENT_SECONDS);

for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
await sleep(RETRY_DELAY_SECONDS);
try {
await checkEnv(appUrl, buildSha);
core.info('✓ env check passed');

await checkOutboundFetch(appUrl);
core.info('✓ outbound fetch check passed');

await checkSecret(appUrl);
core.info('✓ secret check passed');

await checkRequestEcho(appUrl);
core.info('✓ request echo check passed');

for (const mod of checkModules) {
await mod.check(appUrl, ctx);
core.info(`✓ ${mod.name} check passed`);
}
break;
} catch (error) {
core.warning(`Attempt ${attempt} failed: ${error.message}`);
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
core.warning(`Attempt ${attempt} failed: ${detail}`);
if (attempt === MAX_RETRIES) {
const summary = error instanceof Error ? error.message : String(error);
throw new Error(
`Test application invocation failed after ${MAX_RETRIES} attempts: ${error.message}`,
`Test application invocation failed after ${MAX_RETRIES} attempts: ${summary}`,
);
}
}
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/prod-invocation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
- name: Setup Node Environment
uses: ./.github/setup-node

- name: Clean up test-app.wasm
- name: Clean up dist/
shell: bash
run: |
rm -f ./integration-tests/test-application/test-app.wasm
rm -rf ./integration-tests/test-application/dist/

- name: Restore build cache
id: restore-build-cache
Expand Down Expand Up @@ -103,12 +103,12 @@ jobs:
${{ steps.env-check.outputs.is_prod == 'true' &&
steps.secrets.outputs.PROD_GCORE_API_HOSTNAME ||
steps.secrets.outputs.PREPROD_GCORE_API_HOSTNAME }}
wasm_file: integration-tests/test-application/test-app.wasm
wasm_file: integration-tests/test-application/dist/test-app.wasm
app_name: 'fastedge-sdk-js-test-app'
comment: 'Deployed by prod-invocation workflow'
env: |
BUILD_SHA=${{ github.sha }}
TEST_FETCH_URL=https://httpbin.org/json
TEST_FETCH_URL=https://auth.gcore.com/login/assets/config.json
secrets: |
test-secret=${{ steps.create-test-secret.outputs.secret_id }}

Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ bin/
dist/
lib/
integration-tests/test-files/output.wasm
integration-tests/test-application/test-app.wasm
integration-tests/test-application/dist/
# *.o
# *.d
/rusturl/
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Use the decision tree in CONTEXT_INDEX.md to determine what to read. **Only read
| **Adding an example** | Browse `examples/` for similar existing example |
| **Changing build system** | BUILD_SYSTEM + `esbuild/` scripts |
| **Working with WIT** | RUNTIME_ARCHITECTURE (WIT section) + `runtime/FastEdge-wit/` |
| **Bumping StarlingMonkey submodule** | `context/PATCHES.md` (rebase procedure + patch list) |
| **Writing tests** | TESTING_GUIDE |
| **Understanding the system** | PROJECT_OVERVIEW (~150 lines) |
| **Updating docs site** | `github-pages/` (Astro, separate workspace) |
Expand Down
46 changes: 46 additions & 0 deletions context/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,52 @@ When this file grows large, use grep to search — don't read linearly.

---

## [2026-06-09] — Response.clone() full isolation + headers-clone fix; prod guard expanded

### Overview
Completed `Response.clone()`: the body tee now hands each branch independent chunks, and cloning a response after its headers have been read no longer throws. Squashed the Response.clone work into a single commit on `gcore/integration` and re-pinned the submodule. Expanded the prod-invocation guard.

### Changes

**Runtime (StarlingMonkey submodule, `gcore/integration`):**
- `fix(fetch): Response.clone - full clone isolation via cloneForBranch2 tee` — replaces the initial tee (which shared one chunk object between branches) with a hand-rolled `ReadableStreamDefaultTee` (`cloneForBranch2 = true`): branch1 keeps the original chunk, branch2 receives an eager structured clone taken at source-read time. Per-branch cancellation tracked (`canceled1`/`canceled2`).
- Header cloning rewritten to clone via the header handle (`headers_handle_clone`) instead of replaying entries through a guarded append. Fixes `TypeError: Headers are immutable` when cloning an incoming response after its headers were materialised, and preserves multiple `Set-Cookie`.
- The two earlier Response.clone commits (`feat` + wpt expectations) squashed into one; submodule re-pinned `84f5d52` → `702d7a4` (`gcore/integration` = `0.3.0` → Response.clone → blob #311 fix).
- WPT `fetch/api/response/response-clone.any.js` — 21/21.

**Prod-invocation test application:**
- `response-clone` guard extended to 9 sub-tests: host-backed multi-chunk mutation guard (7), cancel-one-branch (8), read-header-then-clone (9).
- `handlers/multi-chunk-source.ts` added — serves a multi-chunk body the guard self-fetches (over https) to exercise a host-backed `HttpIncomingBody` re-segmented across multiple host reads.
- `KNOWN_LIMITATIONS.md` — removed the now-resolved `Response.clone()` tee-bug entry.

---

## [2026-06-08] — Response.clone(), blob.type fix, prod-invocation test infrastructure

### Overview
Implemented `Response.clone()` and `blob.type` propagation in the StarlingMonkey runtime via a `gcore/integration` fork branch. Restructured the prod-invocation test application to be extensible and TypeScript-first.

### Changes

**Runtime (StarlingMonkey submodule):**
- `feat(fetch): implement Response.clone()` — upstream PR #312 (open)
- `fix(fetch): body.blob() sets Blob.type from Content-Type header` — upstream issue #311 (open)
- Submodule now pinned to `godronus/gcore/integration` (SHA `84f5d52`) rather than bare `0.3.0` tag
- `context/PATCHES.md` added — documents applied patches, upstream PR links, rebase procedure, and test guard removal checklist

**Prod-invocation test application (`integration-tests/test-application/`):**
- Converted from a single flat JS file to TypeScript with Hono routing
- Split into `handlers/` (WASM context, `fastedge::` imports) and `checks/` (Node.js, auto-discovered)
- `routes.ts` — single source of truth for route paths and test names
- `types.ts` — shared `CheckContext`, `HandlerModule`, `CheckModule` interfaces
- Build artefacts consolidated under `dist/` (one gitignore entry covers both wasm and compiled checks)
- Temporary `response-clone` handler + check added as regression guard for the StarlingMonkey patches
- `scripts/build-test-app.js` + `scripts/run-test-app-checks.js` — local equivalents of CI scripts
- `pnpm test:app:build` and `pnpm test:app:check` scripts added
- `integration-tests/test-application/README.md` added

---

## [2026-05-05] — Pin host-api bindings to wit-bindgen 0.30.0 (fix wasmtime 36 trap)

### Overview
Expand Down
21 changes: 18 additions & 3 deletions context/CONTEXT_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
| `PROJECT_OVERVIEW.md` | ~150 | Lightweight project overview — architecture, key modules, dev setup, common commands. Read when new to the codebase. |
| `CHANGELOG.md` | ~25+ | Change history. Use grep, don't read linearly as this file grows. |
| `ENHANCEMENTS.md` | ~50 | Known inconsistencies and planned improvements. Read before refactoring related areas. |
| `KNOWN_LIMITATIONS.md` | ~90 | Confirmed runtime gaps — standard Web APIs that look available but don't work on FastEdge (e.g. `Response.error()`). Read when a user reports "X is in the types/spec but throws at runtime". |
| `PATCHES.md` | ~60 | Applied patches on the `gcore/integration` StarlingMonkey branch — upstream PR links, rebase procedure, and retirement steps. Read before touching `runtime/StarlingMonkey`. |

### Plugin Integration (read when modifying manifest or examples)

Expand Down Expand Up @@ -103,11 +105,25 @@
3. Read `runtime/fastedge/host-api/wit/` (local bindings)
4. Run `pnpm run generate:wit-world` after changes

### Bumping the StarlingMonkey Submodule
1. Read `PATCHES.md` — lists applied patches, upstream PR status, and the full rebase procedure
2. Follow the rebase steps there when a new upstream tag lands

### Adding or Modifying a Prod-Invocation Test
1. Read `integration-tests/test-application/README.md` — structure, handler/check split, routes.ts convention
2. Add route constant to `routes.ts`, create `handlers/<name>.ts` + `checks/<name>.ts`, register in `test-app.ts`
3. Test locally: `pnpm test:app:build` then `APP_URL=<url> pnpm test:app:check`

### Writing Tests
1. Read `development/TESTING_GUIDE.md`
2. Follow co-located pattern: `__tests__/` next to source
3. Integration tests go in `integration-tests/`

### "Standard Web API X throws / isn't available at runtime"
1. Read `KNOWN_LIMITATIONS.md` — confirmed runtime gaps + workarounds + upstream tracking
2. Confirm against the builtin: grep `runtime/StarlingMonkey/builtins/web/fetch/request-response.cpp` (or relevant builtin) for the method/symbol
3. Cross-check `types/globals.d.ts` for a commented-out "not implemented by the StarlingMonkey runtime" block

### Understanding the System (new to codebase)
1. Read `PROJECT_OVERVIEW.md` (~150 lines)
2. Skim `architecture/COMPONENTIZE_PIPELINE.md` (pipeline diagram)
Expand Down Expand Up @@ -165,7 +181,6 @@ Items that need attention. Surface these when asked "what's next" or "what needs

These are runtime/Web-API behaviors that have been *requested* in patterns docs but cannot yet be verified against an existing example or runtime test. Build a minimal example app proving each works on FastEdge before adding it to a `docs/` file. If a behavior is **not** supported, capture that here too — negative findings are also documentation.

- **`Response.clone()`** — Standard Web Fetch API. Used by patterns where the upstream body needs to be read twice (e.g. log full response while transforming a copy). No example currently exercises this. Build: a small handler that clones a `fetch()` response and reads both copies. Verify both bodies decode to the same bytes. If it works, the `docs/PROXY_PATTERNS.md` "JSON Transform" section can be expanded to document `clone()` for dual-read patterns.
- **`fetch(url, { redirect: "manual" })`** — Standard Web Fetch option, returns the upstream redirect response without following it. Used by patterns where the app needs to inspect or rewrite the `Location` header. Runtime test harness includes WPT `redirect-mode.any.js` but that does not confirm FastEdge's outbound `fetch` honors the option in production. Build: a handler that issues a `fetch()` to a known 302 endpoint with `redirect: "manual"` and asserts the response is the 302 itself, not the followed target. If it works, the `docs/PROXY_PATTERNS.md` operational notes can call out manual redirect handling.

When adding either to docs, also update the manifest source description so reviewers know the content is now grounded in an example.
Expand All @@ -188,7 +203,7 @@ When adding either to docs, also update the manifest source description so revie
|----------|-----------|-------------|
| Architecture | 2 docs | ~330 |
| Development | 2 docs | ~200 |
| Reference | 2 docs | ~175 |
| **Total** | **6 docs** | **~700** |
| Reference | 4 docs | ~325 |
| **Total** | **8 docs** | **~850** |

All documents are designed for single-sitting reads. No doc exceeds 170 lines.
38 changes: 38 additions & 0 deletions context/KNOWN_LIMITATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Known Runtime Limitations

Confirmed gaps in the StarlingMonkey runtime as it ships in this SDK. These are
**negative findings** — behaviors that look like they should work (standard Web
APIs, types that nearly exist) but do **not** on FastEdge today.

Use this file when a user reports "X is in the types / spec but throws at runtime"
or asks "does FastEdge support X?". For *planned* work and unverified items, see
`ENHANCEMENTS.md` and the "Known Issues / Future Work" section of `CONTEXT_INDEX.md`.

> StarlingMonkey is a git submodule: `runtime/StarlingMonkey` →
> `github.com/bytecodealliance/StarlingMonkey`. The pinned revision determines
> which builtins exist. When the pin moves, re-verify the items below.

---

## `Response.error()` — not implemented

**Status:** Not supported. Also commented out in the same `types/globals.d.ts`
block (`error(): Response; // static`).

**Symptom:** `Response.error()` (the static factory returning a network-error
response) is unavailable.

**Why:** Same root cause — the static method is not registered on `Response` in
the StarlingMonkey fetch builtin. No dedicated upstream issue; track alongside the
general fetch-builtin completeness work.

---

## Adding a new limitation here

1. Confirm it is a *runtime* gap (grep the relevant builtin in
`runtime/StarlingMonkey/builtins/` — absence of the method/symbol).
2. Capture the **why** and any **upstream issue/PR** links.
3. Note the **workaround** and the **uncomment/verify** steps for when it lands.
4. Keep this file a single-sitting read — link out to source rather than
inlining large excerpts.
Loading
Loading