diff --git a/.gitignore b/.gitignore
index 84d33ed..c88d4d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 node_modules
 /lib
 /examples/build
+/examples/package-lock.json
diff --git a/.gitmodules b/.gitmodules
index e7db5ec..b9e3ba9 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,3 @@
-[submodule "third_party/wasi-test-suite"]
-	path = third_party/wasi-test-suite
-	url = https://github.com/caspervonb/wasi-test-suite
+[submodule "third_party/wasi-testsuite"]
+	path = third_party/wasi-testsuite
+	url = https://github.com/WebAssembly/wasi-testsuite
diff --git a/examples/package-lock.json b/examples/package-lock.json
deleted file mode 100644
index 7ae5ca6..0000000
--- a/examples/package-lock.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
-    "name": "examples",
-    "lockfileVersion": 2,
-    "requires": true,
-    "packages": {
-        "": {
-            "dependencies": {
-                "uwasi": "../"
-            }
-        },
-        "..": {
-            "version": "1.1.0",
-            "license": "MIT",
-            "devDependencies": {
-                "@types/jest": "^28.1.4",
-                "@types/node": "^17.0.31",
-                "jest": "^28.1.2",
-                "ts-jest": "^28.0.5",
-                "typescript": "^4.6.4"
-            }
-        },
-        "node_modules/uwasi": {
-            "resolved": "..",
-            "link": true
-        }
-    },
-    "dependencies": {
-        "uwasi": {
-            "version": "file:..",
-            "requires": {
-                "@types/jest": "^28.1.4",
-                "@types/node": "^17.0.31",
-                "jest": "^28.1.2",
-                "ts-jest": "^28.0.5",
-                "typescript": "^4.6.4"
-            }
-        }
-    }
-}
diff --git a/package.json b/package.json
index 53b60e5..1378304 100644
--- a/package.json
+++ b/package.json
@@ -8,8 +8,8 @@
     "./lib/esm/platforms/crypto.js": "./lib/esm/platforms/crypto.browser.js"
   },
   "scripts": {
-    "build": "tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json",
-    "test": "jest",
+    "build": "tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && echo '{ \"type\": \"module\" }' > lib/esm/package.json",
+    "test": "node --test test/*.test.mjs",
     "format": "prettier --write ./src ./test",
     "prepare": "npm run build"
   },
@@ -32,11 +32,8 @@
   "author": "SwiftWasm Team",
   "license": "MIT",
   "devDependencies": {
-    "@types/jest": "^28.1.4",
     "@types/node": "^17.0.31",
-    "jest": "^28.1.2",
     "prettier": "^3.5.2",
-    "ts-jest": "^28.0.5",
     "typescript": "^4.6.4"
   }
 }
diff --git a/src/abi.ts b/src/abi.ts
index 0552232..f4d1e5a 100644
--- a/src/abi.ts
+++ b/src/abi.ts
@@ -62,6 +62,22 @@ export class WASIAbi {
    * The file descriptor or file refers to a regular file inode.
    */
   static readonly WASI_FILETYPE_REGULAR_FILE = 4;
+  /**
+   * Create file if it does not exist.
+   */
+  static readonly WASI_OFLAGS_CREAT = 1 << 0;
+  /**
+   * Open directory.
+   */
+  static readonly WASI_OFLAGS_DIRECTORY = 1 << 1;
+  /**
+   * Fail if not a directory.
+   */
+  static readonly WASI_OFLAGS_EXCL = 1 << 2;
+  /**
+   * Truncate to zero length.
+   */
+  static readonly WASI_OFLAGS_TRUNC = 1 << 3;
 
   static readonly IMPORT_FUNCTIONS = [
     "args_get",
diff --git a/src/features/all.ts b/src/features/all.ts
index 2b4b6a4..8b95d29 100644
--- a/src/features/all.ts
+++ b/src/features/all.ts
@@ -1,29 +1,24 @@
-import { WASIAbi } from "../abi";
-import { WASIFeatureProvider, WASIOptions } from "../options";
-import { useArgs } from "./args";
-import { useClock } from "./clock";
-import { useEnviron } from "./environ";
-import { useFS, useStdio } from "./fd";
-import { useProc } from "./proc";
-import { useRandom } from "./random";
+import { WASIAbi } from "../abi.js";
+import { WASIFeatureProvider, WASIOptions } from "../options.js";
+import { useArgs } from "./args.js";
+import { useClock } from "./clock.js";
+import { useEnviron } from "./environ.js";
+import { useMemoryFS } from "./fd.js";
+import { useProc } from "./proc.js";
+import { useRandom } from "./random.js";
 
-type Options = (Parameters<typeof useFS>[0] | Parameters<typeof useStdio>[0]) &
-  Parameters<typeof useRandom>[0];
+type Options = Parameters<typeof useMemoryFS>[0] & Parameters<typeof useRandom>[0];
 
 export function useAll(useOptions: Options = {}): WASIFeatureProvider {
   return (options: WASIOptions, abi: WASIAbi, memoryView: () => DataView) => {
     const features = [
+      useMemoryFS(useOptions),
       useEnviron,
       useArgs,
       useClock,
       useProc,
       useRandom(useOptions),
     ];
-    if ("fs" in useOptions) {
-      features.push(useFS({ fs: useOptions.fs }));
-    } else {
-      features.push(useStdio(useOptions));
-    }
     return features.reduce((acc, fn) => {
       return { ...acc, ...fn(options, abi, memoryView) };
     }, {});
diff --git a/src/features/args.ts b/src/features/args.ts
index 25acc6e..d947db8 100644
--- a/src/features/args.ts
+++ b/src/features/args.ts
@@ -1,5 +1,5 @@
-import { WASIAbi } from "../abi";
-import { WASIOptions } from "../options";
+import { WASIAbi } from "../abi.js";
+import { WASIOptions } from "../options.js";
 
 /**
  * A feature provider that provides `args_get` and `args_sizes_get`
diff --git a/src/features/clock.ts b/src/features/clock.ts
index cf2fe97..28f5074 100644
--- a/src/features/clock.ts
+++ b/src/features/clock.ts
@@ -1,5 +1,5 @@
-import { WASIAbi } from "../abi";
-import { WASIOptions } from "../options";
+import { WASIAbi } from "../abi.js";
+import { WASIOptions } from "../options.js";
 
 /**
  * A feature provider that provides `clock_res_get` and `clock_time_get` by JavaScript's Date.
diff --git a/src/features/environ.ts b/src/features/environ.ts
index 80e60c0..d6c517d 100644
--- a/src/features/environ.ts
+++ b/src/features/environ.ts
@@ -1,5 +1,5 @@
-import { WASIAbi } from "../abi";
-import { WASIOptions } from "../options";
+import { WASIAbi } from "../abi.js";
+import { WASIOptions } from "../options.js";
 
 /**
  * A feature provider that provides `environ_get` and `environ_sizes_get`
diff --git a/src/features/fd.ts b/src/features/fd.ts
index fa0d34b..acfafed 100644
--- a/src/features/fd.ts
+++ b/src/features/fd.ts
@@ -1,5 +1,5 @@
-import { WASIAbi } from "../abi";
-import { WASIFeatureProvider, WASIOptions } from "../options";
+import { WASIAbi } from "../abi.js";
+import { WASIFeatureProvider, WASIOptions } from "../options.js";
 
 interface FdEntry {
   writev(iovs: Uint8Array[]): number;
@@ -96,7 +96,7 @@ export class ReadableTextProxy implements FdEntry {
   close(): void {}
 }
 
-export type StdIoOptions = {
+export type StdioOptions = {
   stdin?: () => string | Uint8Array;
   stdout?: (lines: string | Uint8Array) => void;
   stderr?: (lines: string | Uint8Array) => void;
@@ -104,7 +104,7 @@ export type StdIoOptions = {
 };
 
 function bindStdio(
-  useOptions: StdIoOptions = {},
+  useOptions: StdioOptions = {},
 ): (ReadableTextProxy | WritableTextProxy)[] {
   const outputBuffers = useOptions.outputBuffers || false;
   return [
@@ -144,7 +144,7 @@ function bindStdio(
  *
  * This provides `fd_write`, `fd_prestat_get` and `fd_prestat_dir_name` implementations to make libc work with minimal effort.
  */
-export function useStdio(useOptions: StdIoOptions = {}): WASIFeatureProvider {
+export function useStdio(useOptions: StdioOptions = {}): WASIFeatureProvider {
   return (options, abi, memoryView) => {
     const fdTable = bindStdio(useOptions);
     return {
@@ -275,13 +275,13 @@ export class MemoryFileSystem {
       const data = new TextEncoder().encode(content);
       this.createFile(path, data);
       return;
-    } else if (content instanceof Blob) {
+    } else if (globalThis.Blob && content instanceof Blob) {
       return content.arrayBuffer().then((buffer) => {
         const data = new Uint8Array(buffer);
         this.createFile(path, data);
       });
     } else {
-      this.createFile(path, content);
+      this.createFile(path, content as Uint8Array);
       return;
     }
   }
@@ -304,7 +304,7 @@ export class MemoryFileSystem {
    * @param node The node to set
    */
   setNode(path: string, node: FSNode): void {
-    const normalizedPath = this.normalizePath(path);
+    const normalizedPath = normalizePath(path);
     const parts = normalizedPath.split("/").filter((p) => p.length > 0);
 
     if (parts.length === 0) {
@@ -345,7 +345,7 @@ export class MemoryFileSystem {
    * @returns The node at the path, or null if not found
    */
   lookup(path: string): FSNode | null {
-    const normalizedPath = this.normalizePath(path);
+    const normalizedPath = normalizePath(path);
     if (normalizedPath === "/") return this.root;
 
     const parts = normalizedPath.split("/").filter((p) => p.length > 0);
@@ -367,7 +367,7 @@ export class MemoryFileSystem {
    * @returns The resolved node, or null if not found
    */
   resolve(dir: DirectoryNode, relativePath: string): FSNode | null {
-    const normalizedPath = this.normalizePath(relativePath);
+    const normalizedPath = normalizePath(relativePath);
     const parts = normalizedPath.split("/").filter((p) => p.length > 0);
     let current: FSNode = dir;
 
@@ -391,7 +391,7 @@ export class MemoryFileSystem {
    * @returns The directory node
    */
   ensureDir(path: string): DirectoryNode {
-    const normalizedPath = this.normalizePath(path);
+    const normalizedPath = normalizePath(path);
     const parts = normalizedPath.split("/").filter((p) => p.length > 0);
     let current: DirectoryNode = this.root;
 
@@ -418,7 +418,7 @@ export class MemoryFileSystem {
    * @returns The created file node
    */
   createFileIn(dir: DirectoryNode, relativePath: string): FileNode {
-    const normalizedPath = this.normalizePath(relativePath);
+    const normalizedPath = normalizePath(relativePath);
     const parts = normalizedPath.split("/").filter((p) => p.length > 0);
 
     if (parts.length === 0) {
@@ -446,24 +446,45 @@ export class MemoryFileSystem {
     return fileNode;
   }
 
-  /**
-   * Normalizes a path by removing duplicate slashes and trailing slashes.
-   * @param path Path to normalize
-   * @returns Normalized path
-   */
-  private normalizePath(path: string): string {
-    // Handle empty path
-    if (!path) return "/";
-
-    // Ensure path starts with a slash
-    const withLeadingSlash = path.startsWith("/") ? path : `/${path}`;
+  removeEntry(path: string): void {
+    const normalizedPath = normalizePath(path);
+    const parts = normalizedPath.split("/").filter((p) => p.length > 0);
+    let parentDir = this.root;
+    for (let i = 0; i < parts.length - 1; i++) {
+      const part = parts[i];
+      if (parentDir.type !== "dir") return;
+      parentDir = parentDir.entries[part] as DirectoryNode;
+    }
 
-    // Remove duplicate slashes and normalize
-    const normalized = withLeadingSlash.replace(/\/+/g, "/");
+    const fileName = parts[parts.length - 1];
+    delete parentDir.entries[fileName];
+  }
+}
 
-    // Remove trailing slash unless it's the root path
-    return normalized === "/" ? normalized : normalized.replace(/\/+$/, "");
+/**
+ * Normalizes a path by removing duplicate slashes and trailing slashes.
+ * @param path Path to normalize
+ * @returns Normalized path
+ */
+function normalizePath(path: string): string {
+  // Handle empty path
+  if (!path) return "/";
+
+  const parts = path.split("/").filter((p) => p.length > 0);
+  const normalizedParts: string[] = [];
+
+  for (const part of parts) {
+    if (part === ".") continue;
+    if (part === "..") {
+      normalizedParts.pop();
+      continue;
+    }
+    normalizedParts.push(part);
   }
+  if (normalizedParts.length === 0) return "/";
+
+  const normalized = "/" + normalizedParts.join("/");
+  return normalized;
 }
 
 /**
@@ -496,7 +517,7 @@ export class MemoryFileSystem {
  * const wasi = new WASI({
  *   features: [
  *     useMemoryFS({
- *       withStdIo: {
+ *       withStdio: {
  *         stdout: (lines) => document.write(lines),
  *         stderr: (lines) => document.write(lines),
  *       }
@@ -507,13 +528,13 @@ export class MemoryFileSystem {
  *
  * @param useOptions - Configuration options for the memory file system
  * @param useOptions.withFileSystem - Optional pre-configured file system instance
- * @param useOptions.withStdIo - Optional standard I/O configuration
+ * @param useOptions.withStdio - Optional standard I/O configuration
  * @returns A WASI feature provider implementing file system functionality
  */
 export function useMemoryFS(
   useOptions: {
     withFileSystem?: MemoryFileSystem;
-    withStdIo?: StdIoOptions;
+    withStdio?: StdioOptions;
   } = {},
 ): WASIFeatureProvider {
   return (
@@ -525,7 +546,7 @@ export function useMemoryFS(
       useOptions.withFileSystem || new MemoryFileSystem(wasiOptions.preopens);
     const files: { [fd: FileDescriptor]: OpenFile } = {};
 
-    bindStdio(useOptions.withStdIo || {}).forEach((entry, fd) => {
+    bindStdio(useOptions.withStdio || {}).forEach((entry, fd) => {
       files[fd] = {
         node: { type: "character", kind: "stdio", entry },
         position: 0,
@@ -803,8 +824,9 @@ export function useMemoryFS(
         return WASIAbi.WASI_ESUCCESS;
       },
 
-      fd_open: (
+      path_open: (
         dirfd: number,
+        _dirflags: number,
         pathPtr: number,
         pathLen: number,
         oflags: number,
@@ -823,9 +845,9 @@ export function useMemoryFS(
 
         const path = abi.readString(view, pathPtr, pathLen);
 
-        const guestPath =
-          (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") +
-          path;
+        const guestPath = normalizePath(
+          (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") + path,
+        );
 
         const existing = getFileFromPath(guestPath);
         if (existing) {
@@ -833,20 +855,20 @@ export function useMemoryFS(
           return WASIAbi.WASI_ESUCCESS;
         }
 
-        let target = fileSystem.resolve(dirEntry.node, path);
-        const O_CREAT = 1 << 0,
-          O_EXCL = 1 << 1,
-          O_TRUNC = 1 << 2;
+        let target = fileSystem.resolve(dirEntry.node as DirectoryNode, path);
 
         if (target) {
-          if (oflags & O_EXCL) return WASIAbi.WASI_ERRNO_EXIST;
-          if (oflags & O_TRUNC) {
+          if (oflags & WASIAbi.WASI_OFLAGS_EXCL) return WASIAbi.WASI_ERRNO_EXIST;
+          if (oflags & WASIAbi.WASI_OFLAGS_TRUNC) {
             if (target.type !== "file") return WASIAbi.WASI_ERRNO_INVAL;
-            target.content = new Uint8Array(0);
+            (target as FileNode).content = new Uint8Array(0);
           }
         } else {
-          if (!(oflags & O_CREAT)) return WASIAbi.WASI_ERRNO_NOENT;
-          target = fileSystem.createFileIn(dirEntry.node, path);
+          if (!(oflags & WASIAbi.WASI_OFLAGS_CREAT)) return WASIAbi.WASI_ERRNO_NOENT;
+          target = fileSystem.createFileIn(
+            dirEntry.node as DirectoryNode,
+            path,
+          );
         }
 
         files[nextFd] = {
@@ -862,66 +884,48 @@ export function useMemoryFS(
         return WASIAbi.WASI_ESUCCESS;
       },
 
-      path_open: (
-        dirfd: number,
-        _dirflags: number,
-        pathPtr: number,
-        pathLen: number,
-        oflags: number,
-        _fs_rights_base: bigint,
-        _fs_rights_inheriting: bigint,
-        _fdflags: number,
-        opened_fd: number,
-      ) => {
+      path_create_directory: (fd: number, pathPtr: number, pathLen: number) => {
         const view = memoryView();
+        const guestRelPath = abi.readString(view, pathPtr, pathLen);
+        const dirEntry = getFileFromFD(fd);
+        if (!dirEntry || dirEntry.node.type !== "dir")
+          return WASIAbi.WASI_ERRNO_NOTDIR;
 
-        if (dirfd < 3) return WASIAbi.WASI_ERRNO_NOTDIR;
+        const fullGuestPath =
+          (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") +
+          guestRelPath;
 
-        const dirEntry = getFileFromFD(dirfd);
+        fileSystem.ensureDir(fullGuestPath);
+        return WASIAbi.WASI_ESUCCESS;
+      },
+
+      path_unlink_file: (fd: number, pathPtr: number, pathLen: number) => {
+        const view = memoryView();
+        const guestRelPath = abi.readString(view, pathPtr, pathLen);
+        const dirEntry = getFileFromFD(fd);
         if (!dirEntry || dirEntry.node.type !== "dir")
           return WASIAbi.WASI_ERRNO_NOTDIR;
 
-        const path = abi.readString(view, pathPtr, pathLen);
-
-        const guestPath =
+        const fullGuestPath =
           (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") +
-          path;
-
-        const existing = getFileFromPath(guestPath);
-        if (existing) {
-          view.setUint32(opened_fd, existing.fd, true);
-          return WASIAbi.WASI_ESUCCESS;
-        }
+          guestRelPath;
 
-        let target = fileSystem.resolve(dirEntry.node as DirectoryNode, path);
-        const O_CREAT = 1 << 0,
-          O_EXCL = 1 << 1,
-          O_TRUNC = 1 << 2;
+        fileSystem.removeEntry(fullGuestPath);
+        return WASIAbi.WASI_ESUCCESS;
+      },
 
-        if (target) {
-          if (oflags & O_EXCL) return WASIAbi.WASI_ERRNO_EXIST;
-          if (oflags & O_TRUNC) {
-            if (target.type !== "file") return WASIAbi.WASI_ERRNO_INVAL;
-            (target as FileNode).content = new Uint8Array(0);
-          }
-        } else {
-          if (!(oflags & O_CREAT)) return WASIAbi.WASI_ERRNO_NOENT;
-          target = fileSystem.createFileIn(
-            dirEntry.node as DirectoryNode,
-            path,
-          );
-        }
+      path_remove_directory: (fd: number, pathPtr: number, pathLen: number) => {
+        const view = memoryView();
+        const guestRelPath = abi.readString(view, pathPtr, pathLen);
+        const dirEntry = getFileFromFD(fd);
+        if (!dirEntry || dirEntry.node.type !== "dir")
+          return WASIAbi.WASI_ERRNO_NOTDIR;
 
-        files[nextFd] = {
-          node: target,
-          position: 0,
-          isPreopen: false,
-          path: guestPath,
-          fd: nextFd,
-        };
+        const fullGuestPath =
+          (dirEntry.path.endsWith("/") ? dirEntry.path : dirEntry.path + "/") +
+          guestRelPath;
 
-        view.setUint32(opened_fd, nextFd, true);
-        nextFd++;
+        fileSystem.removeEntry(fullGuestPath);
         return WASIAbi.WASI_ESUCCESS;
       },
 
diff --git a/src/features/proc.ts b/src/features/proc.ts
index 6bb3476..94f0c55 100644
--- a/src/features/proc.ts
+++ b/src/features/proc.ts
@@ -1,5 +1,5 @@
-import { WASIAbi, WASIProcExit } from "../abi";
-import { WASIOptions } from "../options";
+import { WASIAbi, WASIProcExit } from "../abi.js";
+import { WASIOptions } from "../options.js";
 
 /**
  * A feature provider that provides `proc_exit` and `proc_raise` by JavaScript's exception.
diff --git a/src/features/random.ts b/src/features/random.ts
index 420118e..05208ac 100644
--- a/src/features/random.ts
+++ b/src/features/random.ts
@@ -1,6 +1,6 @@
-import { WASIAbi } from "../abi";
-import { WASIFeatureProvider } from "../options";
-import { defaultRandomFillSync } from "../platforms/crypto";
+import { WASIAbi } from "../abi.js";
+import { WASIFeatureProvider } from "../options.js";
+import { defaultRandomFillSync } from "../platforms/crypto.js";
 
 /**
  * Create a feature provider that provides `random_get` with `crypto` APIs as backend by default.
diff --git a/src/features/tracing.ts b/src/features/tracing.ts
index 0ff9cc2..dbf8bf2 100644
--- a/src/features/tracing.ts
+++ b/src/features/tracing.ts
@@ -1,4 +1,4 @@
-import { WASIFeatureProvider } from "../options";
+import { WASIFeatureProvider } from "../options.js";
 
 export function useTrace(features: WASIFeatureProvider[]): WASIFeatureProvider {
   return (options, abi, memoryView) => {
diff --git a/src/index.ts b/src/index.ts
index d69248f..24164d0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,15 +1,15 @@
-import { WASIAbi, WASIProcExit } from "./abi";
-export { WASIProcExit } from "./abi";
-import { WASIOptions } from "./options";
+import { WASIAbi, WASIProcExit } from "./abi.js";
+export { WASIProcExit } from "./abi.js";
+import { WASIOptions } from "./options.js";
 
-export * from "./features/all";
-export * from "./features/args";
-export * from "./features/clock";
-export * from "./features/environ";
-export { useFS, useStdio, useMemoryFS, MemoryFileSystem } from "./features/fd";
-export * from "./features/proc";
-export * from "./features/random";
-export * from "./features/tracing";
+export * from "./features/all.js";
+export * from "./features/args.js";
+export * from "./features/clock.js";
+export * from "./features/environ.js";
+export { useFS, useStdio, useMemoryFS, MemoryFileSystem } from "./features/fd.js";
+export * from "./features/proc.js";
+export * from "./features/random.js";
+export * from "./features/tracing.js";
 
 export class WASI {
   /**
diff --git a/src/options.ts b/src/options.ts
index 76d7f6f..ba9b177 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -1,4 +1,4 @@
-import { WASIAbi } from "./abi";
+import { WASIAbi } from "./abi.js";
 
 export type WASIFeatureProvider = (
   options: WASIOptions,
diff --git a/test/fd.test.ts b/test/fd.test.mjs
similarity index 64%
rename from test/fd.test.ts
rename to test/fd.test.mjs
index fa4aeaa..7bb6e0f 100644
--- a/test/fd.test.ts
+++ b/test/fd.test.mjs
@@ -1,4 +1,6 @@
-import { ReadableTextProxy } from "../src/features/fd";
+import { ReadableTextProxy } from "../lib/esm/features/fd.js";
+import { describe, it } from 'node:test';
+import assert from 'node:assert';
 
 describe("fd.ReadableTextProxy", () => {
   it("readv single buffer", () => {
@@ -7,9 +9,9 @@ describe("fd.ReadableTextProxy", () => {
     const proxy = new ReadableTextProxy(() => inputs.shift() || "");
     const buffer = new Uint8Array(10);
     const read = proxy.readv([buffer]);
-    expect(read).toBe(5);
+    assert.strictEqual(read, 5);
     const expected = new TextEncoder().encode(input);
-    expect(buffer.slice(0, 5)).toEqual(expected);
+    assert.deepStrictEqual(buffer.slice(0, 5), expected);
   });
 
   it("readv 2 buffer", () => {
@@ -19,9 +21,9 @@ describe("fd.ReadableTextProxy", () => {
     const buf0 = new Uint8Array(2);
     const buf1 = new Uint8Array(2);
     const read = proxy.readv([buf0, buf1]);
-    expect(read).toBe(4);
+    assert.strictEqual(read, 4);
     const expected = new TextEncoder().encode(input);
-    expect(buf0).toEqual(expected.slice(0, 2));
-    expect(buf1).toEqual(expected.slice(2, 4));
+    assert.deepStrictEqual(buf0, expected.slice(0, 2));
+    assert.deepStrictEqual(buf1, expected.slice(2, 4));
   });
 });
diff --git a/test/wasi-test-suite/core.test.ts b/test/wasi-test-suite/core.test.ts
deleted file mode 100644
index 7c75baa..0000000
--- a/test/wasi-test-suite/core.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { readdirSync, statSync } from "fs";
-import { join as pathJoin } from "path";
-import { runTest } from "./harness";
-
-describe("wasi-test-suite-core", () => {
-  const suiteDir = pathJoin(
-    __dirname,
-    "../../third_party/wasi-test-suite/core",
-  );
-  const entries = readdirSync(suiteDir);
-  const UNSUPPORTED = [
-    "fd_stat_get-stderr.wasm",
-    "fd_stat_get-stdin.wasm",
-    "fd_stat_get-stdout.wasm",
-    "sched_yield.wasm",
-  ];
-
-  for (const entry of entries) {
-    const filePath = pathJoin(suiteDir, entry);
-    const stat = statSync(filePath);
-    if (!entry.endsWith(".wasm") || !stat.isFile()) {
-      continue;
-    }
-    const defineCase = UNSUPPORTED.includes(entry) ? it.skip : it;
-    defineCase(entry, async () => {
-      await runTest(filePath);
-    });
-  }
-});
diff --git a/test/wasi-test-suite/harness.ts b/test/wasi-test-suite/harness.ts
deleted file mode 100644
index c4c52cb..0000000
--- a/test/wasi-test-suite/harness.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { WASI, useAll } from "../../src/index";
-import { WASIAbi } from "../../src/abi";
-import { existsSync } from "fs";
-import { readFile } from "fs/promises";
-import { basename } from "path";
-
-export async function runTest(filePath: string) {
-  let stdout = "";
-  let stderr = "";
-  let stdin = await (async () => {
-    const path = filePath.replace(/\.wasm$/, ".stdin");
-    if (!existsSync(path)) {
-      return "";
-    }
-    return await readFile(path, "utf8");
-  })();
-  const features = [
-    useAll({
-      stdin: () => {
-        const result = stdin;
-        stdin = "";
-        return result;
-      },
-      stdout: (lines) => {
-        stdout += lines;
-      },
-      stderr: (lines) => {
-        stderr += lines;
-      },
-    }),
-  ];
-  const env = await (async () => {
-    const path = filePath.replace(/\.wasm$/, ".env");
-    if (!existsSync(path)) {
-      return {};
-    }
-    const data = await readFile(path, "utf8");
-    return data.split("\n").reduce((acc, line) => {
-      const components = line.trim().split("=");
-      if (components.length < 2) {
-        return acc;
-      }
-      return { ...acc, [components[0]]: components.slice(1).join("=") };
-    }, {});
-  })();
-  const args = await (async () => {
-    const path = filePath.replace(/\.wasm$/, ".arg");
-    if (!existsSync(path)) {
-      return [];
-    }
-    const data = await readFile(path, "utf8");
-    return data
-      .split("\n")
-      .map((line) => line.trim())
-      .filter((line) => line.length > 0);
-  })();
-  const wasi = new WASI({
-    args: [basename(filePath)].concat(args),
-    env,
-    features: features,
-  });
-  const { instance } = await WebAssembly.instantiate(await readFile(filePath), {
-    wasi_snapshot_preview1: wasi.wasiImport,
-  });
-  let exitCode: number;
-  try {
-    exitCode = wasi.start(instance);
-  } catch (e) {
-    if (e instanceof WebAssembly.RuntimeError && e.message == "unreachable") {
-      // When unreachable code is executed, many WebAssembly runtimes raise
-      // SIGABRT (=0x6) signal. It results in exit code 0x80 + signal number in shell.
-      // Reference: https://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
-      exitCode = 0x86;
-    } else {
-      throw e;
-    }
-  }
-  const expectedExitCode = await (async () => {
-    const path = filePath.replace(/\.wasm$/, ".status");
-    if (!existsSync(path)) {
-      return WASIAbi.WASI_ESUCCESS;
-    }
-    return parseInt(await readFile(path, { encoding: "utf-8" }), 10);
-  })();
-  const expectedStdout = await (async () => {
-    const path = filePath.replace(/\.wasm$/, ".stdout");
-    if (!existsSync(path)) {
-      return null;
-    }
-    return await readFile(path, { encoding: "utf-8" });
-  })();
-  const expectedStderr = await (async () => {
-    const path = filePath.replace(/\.wasm$/, ".stderr");
-    if (!existsSync(path)) {
-      return null;
-    }
-    return await readFile(path, { encoding: "utf-8" });
-  })();
-  expect(exitCode).toBe(expectedExitCode);
-  if (expectedStdout) {
-    expect(stdout).toBe(expectedStdout);
-  }
-  if (expectedStderr) {
-    expect(stderr).toBe(expectedStderr);
-  }
-}
diff --git a/test/wasi-test-suite/libc.test.ts b/test/wasi-test-suite/libc.test.ts
deleted file mode 100644
index 2115c17..0000000
--- a/test/wasi-test-suite/libc.test.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { readdirSync, statSync } from "fs";
-import { join as pathJoin } from "path";
-import { runTest } from "./harness";
-
-describe("wasi-test-suite-libc", () => {
-  const suiteDir = pathJoin(
-    __dirname,
-    "../../third_party/wasi-test-suite/libc",
-  );
-  const entries = readdirSync(suiteDir);
-  const UNSUPPORTED = ["clock_gettime-monotonic.wasm", "ftruncate.wasm"];
-
-  for (const entry of entries) {
-    const filePath = pathJoin(suiteDir, entry);
-    const stat = statSync(filePath);
-    if (!entry.endsWith(".wasm") || !stat.isFile()) {
-      continue;
-    }
-    const defineCase = UNSUPPORTED.includes(entry) ? it.skip : it;
-    defineCase(entry, async () => {
-      await runTest(filePath);
-    });
-  }
-});
diff --git a/test/wasi-test-suite/libstd.test.ts b/test/wasi-test-suite/libstd.test.ts
deleted file mode 100644
index 34b4e55..0000000
--- a/test/wasi-test-suite/libstd.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { readdirSync, statSync } from "fs";
-import { join as pathJoin } from "path";
-import { runTest } from "./harness";
-
-describe("wasi-test-suite-libstd", () => {
-  const suiteDir = pathJoin(
-    __dirname,
-    "../../third_party/wasi-test-suite/libstd",
-  );
-  const entries = readdirSync(suiteDir);
-  const UNSUPPORTED = [
-    "fs_create_dir-new-directory.wasm",
-    "fs_file_create.wasm",
-    "fs_metadata-directory.wasm",
-    "fs_metadata-file.wasm",
-    "fs_rename-directory.wasm",
-    "fs_rename-file.wasm",
-  ];
-
-  for (const entry of entries) {
-    const filePath = pathJoin(suiteDir, entry);
-    const stat = statSync(filePath);
-    if (!entry.endsWith(".wasm") || !stat.isFile()) {
-      continue;
-    }
-    const defineCase = UNSUPPORTED.includes(entry) ? it.skip : it;
-    defineCase(entry, async () => {
-      await runTest(filePath);
-    });
-  }
-});
diff --git a/test/wasi.skip.json b/test/wasi.skip.json
new file mode 100644
index 0000000..b2f476d
--- /dev/null
+++ b/test/wasi.skip.json
@@ -0,0 +1,54 @@
+{
+  "WASI Rust tests": {
+    "sched_yield": "not implemented yet",
+    "poll_oneoff_stdio": "not implemented yet",
+    "path_rename": "not implemented yet",
+    "fd_advise": "not implemented yet",
+    "path_exists": "not implemented yet",
+    "path_open_dirfd_not_dir": "not implemented yet",
+    "fd_filestat_set": "not implemented yet",
+    "symlink_create": "not implemented yet",
+    "overwrite_preopen": "not implemented yet",
+    "path_open_read_write": "not implemented yet",
+    "path_rename_dir_trailing_slashes": "not implemented yet",
+    "fd_flags_set": "not implemented yet",
+    "path_filestat": "not implemented yet",
+    "path_link": "not implemented yet",
+    "fd_fdstat_set_rights": "not implemented yet",
+    "readlink": "not implemented yet",
+    "unlink_file_trailing_slashes": "not implemented yet",
+    "path_symlink_trailing_slashes": "not implemented yet",
+    "dangling_symlink": "not implemented yet",
+    "dir_fd_op_failures": "not implemented yet",
+    "file_allocate": "not implemented yet",
+    "nofollow_errors": "not implemented yet",
+    "path_open_preopen": "not implemented yet",
+    "fd_readdir": "not implemented yet",
+    "directory_seek": "not implemented yet",
+    "symlink_filestat": "not implemented yet",
+    "symlink_loop": "not implemented yet",
+    "interesting_paths": "not implemented yet",
+
+    "stdio": "fail",
+    "renumber": "fail",
+    "remove_nonempty_directory": "fail",
+    "remove_directory_trailing_slashes": "fail",
+    "fstflags_validate": "fail",
+    "file_unbuffered_write": "fail",
+    "file_seek_tell": "fail",
+    "file_pread_pwrite": "fail",
+    "close_preopen": "fail"
+  },
+  "WASI C tests": {
+    "sock_shutdown-invalid_fd": "not implemented yet",
+    "stat-dev-ino": "not implemented yet",
+    "sock_shutdown-not_sock": "not implemented yet",
+    "fdopendir-with-access": "not implemented yet",
+
+    "pwrite-with-append": "fail",
+    "pwrite-with-access": "fail",
+    "pread-with-access": "fail"
+  },
+  "WASI AssemblyScript tests": {
+  }
+} 
diff --git a/test/wasi.test.mjs b/test/wasi.test.mjs
new file mode 100644
index 0000000..33fc4a2
--- /dev/null
+++ b/test/wasi.test.mjs
@@ -0,0 +1,213 @@
+// @ts-check
+import fs from 'fs/promises';
+import fsSync from 'fs';
+import path from 'path';
+import { useAll, WASI, MemoryFileSystem } from '../lib/esm/index.js';
+import { describe, it } from 'node:test';
+import assert from 'node:assert';
+
+/**
+ * @typedef {{ exit_code?: number, args?: string[], env?: Record<string, string>, dirs?: string[] }} TestCaseConfig
+ * @typedef {{ suite: string, wasmFile: string, testName: string, config: TestCaseConfig }} TestCase
+ */
+
+/**
+ * Helper function to find test cases directory and files
+ *
+ * @param {string} testDir - The directory to search for test cases
+ * @returns {Array<TestCase>} An array of test cases
+ */
+function findTestCases(testDir) {
+    const testSuites = [
+        { path: 'rust/testsuite', name: 'WASI Rust tests' },
+        { path: 'c/testsuite', name: 'WASI C tests' },
+        { path: 'assemblyscript/testsuite', name: 'WASI AssemblyScript tests' },
+    ];
+
+    /** @type {Array<TestCase>} */
+    const allTests = [];
+
+    for (const suite of testSuites) {
+        const suitePath = path.join(testDir, suite.path);
+        try {
+            const files = fsSync.readdirSync(suitePath);
+            const wasmFiles = files.filter(file => file.endsWith('.wasm'));
+
+            for (const wasmFile of wasmFiles) {
+                // Find corresponding JSON config file
+                const jsonFile = wasmFile.replace('.wasm', '.json');
+                const jsonPath = path.join(suitePath, jsonFile);
+
+                let config = {};
+                try {
+                    const jsonContent = fsSync.readFileSync(jsonPath, 'utf8');
+                    config = JSON.parse(jsonContent);
+                } catch (e) {
+                    // Use default config if no JSON config file found
+                    config = {};
+                }
+
+                allTests.push({
+                    suite: suite.name,
+                    wasmFile: path.join(suitePath, wasmFile),
+                    testName: path.basename(wasmFile, '.wasm'),
+                    config
+                });
+            }
+        } catch (err) {
+            console.warn(`Test suite ${suite.name} is not available: ${err.message}`);
+        }
+    }
+
+    return allTests;
+}
+
+// Helper function to run a test
+async function runTest(testCase) {
+    /** @type {string[]} */
+    const args = [];
+    /** @type {Record<string, string>} */
+    const env = {};
+
+    // Add args if specified
+    if (testCase.config.args) {
+        args.push(...testCase.config.args);
+    }
+
+    // Add env if specified
+    if (testCase.config.env) {
+        for (const [key, value] of Object.entries(testCase.config.env)) {
+            env[key] = value;
+        }
+    }
+
+    // Setup file system
+    const fileSystem = new MemoryFileSystem((testCase.config.dirs || []).reduce((obj, dir) => {
+        obj[dir] = dir;
+        return obj;
+    }, {}));
+
+    // Clone directories to memory file system
+    if (testCase.config.dirs) {
+        for (const dir of testCase.config.dirs) {
+            const dirPath = path.join(path.dirname(testCase.wasmFile), dir);
+            await cloneDirectoryToMemFS(fileSystem, dirPath, "/" + dir);
+        }
+    }
+
+    // Create stdout and stderr buffers
+    let stdoutData = '';
+    let stderrData = '';
+
+    // Create WASI instance
+    const wasi = new WASI({
+        args: [path.basename(testCase.wasmFile), ...args],
+        env: env,
+        features: [
+            useAll({
+                withFileSystem: fileSystem,
+                withStdio: {
+                    stdout: (data) => {
+                        if (typeof data === 'string') {
+                            stdoutData += data;
+                        } else {
+                            stdoutData += new TextDecoder().decode(data);
+                        }
+                    },
+                    stderr: (data) => {
+                        if (typeof data === 'string') {
+                            stderrData += data;
+                        } else {
+                            stderrData += new TextDecoder().decode(data);
+                        }
+                    }
+                }
+            }),
+        ],
+    });
+
+    try {
+        const wasmBytes = await fs.readFile(testCase.wasmFile);
+        const wasmModule = await WebAssembly.compile(wasmBytes);
+        const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
+        const instance = await WebAssembly.instantiate(wasmModule, importObject);
+
+        // Start WASI
+        const exitCode = wasi.start(instance);
+
+        return {
+            exitCode,
+            stdout: stdoutData,
+            stderr: stderrData
+        };
+    } catch (error) {
+        return {
+            error: error.message,
+            exitCode: 1,
+            stdout: stdoutData,
+            stderr: stderrData
+        };
+    }
+}
+
+/**
+ * Helper function to clone a directory to memory file system
+ * 
+ * @param {MemoryFileSystem} fileSystem 
+ * @param {string} sourceDir 
+ * @param {string} targetPath 
+ * @returns {Promise<void>}
+ */
+async function cloneDirectoryToMemFS(fileSystem, sourceDir, targetPath) {
+    // Check if directory exists
+    const stats = await fs.stat(sourceDir);
+    if (!stats.isDirectory()) {
+        return;
+    }
+
+    // Create directory in file system
+    fileSystem.ensureDir(targetPath);
+
+    // Read directory contents
+    const entries = await fs.readdir(sourceDir, { withFileTypes: true });
+
+    // Process each entry
+    for (const entry of entries) {
+        const sourcePath = path.join(sourceDir, entry.name);
+        const targetFilePath = path.join(targetPath, entry.name);
+
+        if (entry.isDirectory()) {
+            // Recursively clone directory
+            await cloneDirectoryToMemFS(fileSystem, sourcePath, targetFilePath);
+        } else if (entry.isFile()) {
+            // Read file content and add to file system
+            const content = await fs.readFile(sourcePath);
+            fileSystem.addFile(targetFilePath, content);
+        }
+    }
+}
+
+// Main test setup
+describe('WASI Test Suite', () => {
+    const __dirname = path.dirname(new URL(import.meta.url).pathname);
+    const testDir = path.join(__dirname, '../third_party/wasi-testsuite/tests');
+    const testCases = findTestCases(testDir);
+    // Load the skip tests list
+    let skipTests = {};
+    try {
+        skipTests = JSON.parse(fsSync.readFileSync(path.join(__dirname, './wasi.skip.json'), 'utf8'));
+    } catch (err) {
+        console.warn('Could not load skip tests file. All tests will be run.');
+    }
+
+    // This test will dynamically create and run tests for each test case
+    for (const testCase of testCases) {
+        const isSkipped = skipTests[testCase.suite] && skipTests[testCase.suite][testCase.testName];
+        const defineTest = isSkipped ? it.skip : it;
+        defineTest(`${testCase.suite} - ${testCase.testName}`, async () => {
+            const result = await runTest(testCase);
+            assert.strictEqual(result.error, undefined, result.stderr);
+            assert.strictEqual(result.exitCode, testCase.config.exit_code || 0, result.stderr);
+        });
+    }
+}); 
diff --git a/third_party/wasi-test-suite b/third_party/wasi-test-suite
deleted file mode 160000
index 89cd4de..0000000
--- a/third_party/wasi-test-suite
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 89cd4de0a260931308999750259ad760cd2db23d
diff --git a/third_party/wasi-testsuite b/third_party/wasi-testsuite
new file mode 160000
index 0000000..946ee51
--- /dev/null
+++ b/third_party/wasi-testsuite
@@ -0,0 +1 @@
+Subproject commit 946ee51fcefe1b561f87e0a1799458a803634f1f