diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fcaaa030d..3b4486d336 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -675,6 +675,8 @@ jobs: main_pr_only: ${{ matrix.main_pr_only }} fetch_depth: ${{ matrix.fetch_depth == 'full' && '0' || '1' }} timeout_minutes: ${{ matrix.timeout_minutes || 360 }} + secrets: + CLAW_API_KEY: ${{ secrets.CLAW_API_KEY }} publish_base_container: name: Publish base container image diff --git a/.github/workflows/reusable-command.yml b/.github/workflows/reusable-command.yml index d37a8bca57..38f37634de 100644 --- a/.github/workflows/reusable-command.yml +++ b/.github/workflows/reusable-command.yml @@ -52,6 +52,9 @@ on: required: false type: number default: 360 + secrets: + CLAW_API_KEY: + required: false jobs: run_host: if: >- @@ -81,6 +84,7 @@ jobs: - name: Run command env: STARRY_APK_REGION: ${{ inputs.apk_region }} + CLAW_API_KEY: ${{ secrets.CLAW_API_KEY }} run: ${{ inputs.command }} run_container: @@ -152,4 +156,5 @@ jobs: - name: Run command env: STARRY_APK_REGION: ${{ inputs.apk_region }} + CLAW_API_KEY: ${{ secrets.CLAW_API_KEY }} run: ${{ inputs.command }} diff --git a/apps/starry/claw-code/build-x86_64-unknown-none.toml b/apps/starry/claw-code/build-x86_64-unknown-none.toml new file mode 100644 index 0000000000..1128aa2eb9 --- /dev/null +++ b/apps/starry/claw-code/build-x86_64-unknown-none.toml @@ -0,0 +1,14 @@ +target = "x86_64-unknown-none" +env = {} +log = "Warn" +features = [ + "ax-hal/x86-pc", + "qemu", + "ax-driver/plat-static", + "ax-driver/virtio-blk", + "ax-driver/virtio-net", + "ax-driver/virtio-gpu", + "ax-driver/virtio-input", + "ax-driver/virtio-socket", +] +plat_dyn = false diff --git a/apps/starry/claw-code/prebuild.sh b/apps/starry/claw-code/prebuild.sh new file mode 100755 index 0000000000..49c4042856 --- /dev/null +++ b/apps/starry/claw-code/prebuild.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +CLAW_REPO="https://github.com/MuZhao2333/claw-code" +CACHE_DIR="${CLAW_CACHE_DIR:-${HOME}/.cache/claw-code-build}" +CLAW_SRC="$CACHE_DIR/repo" +TARGET="x86_64-unknown-linux-musl" +CLAW_BIN="$CACHE_DIR/claw" + +WORKSPACE="${STARRY_WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}" +ROOTFS_DIR="$WORKSPACE/tmp/axbuild/rootfs" +OVERLAY="${STARRY_OVERLAY_DIR:-$WORKSPACE/tmp/axbuild/starry-app/claw-code/overlay}" + +echo "=== 1. Build claw from source ===" +if [ -f "$CLAW_BIN" ]; then + echo "claw binary cached at $CLAW_BIN" +else + mkdir -p "$CACHE_DIR" + rustup target add "$TARGET" 2>/dev/null || true + if [ ! -d "$CLAW_SRC" ]; then + echo "Cloning $CLAW_REPO ..." + git clone --depth 1 "$CLAW_REPO" "$CLAW_SRC" + fi + echo "Building claw for $TARGET (this may take a while)..." + ( + cd "$CLAW_SRC/rust" + cargo build --workspace --release --target "$TARGET" --target-dir "$CACHE_DIR/target" + ) + cp "$CACHE_DIR/target/$TARGET/release/claw" "$CLAW_BIN" + chmod +x "$CLAW_BIN" + echo "claw binary built: $CLAW_BIN" +fi + +echo "=== 2. Prepare rootfs ===" +# The app framework may have created rootfs-x86_64-claw-code.img via fs::copy, +# which can leave a stale lock preventing QEMU from opening it. Delete the +# target first, then copy the base alpine image afresh. +ALPINE_IMG="$ROOTFS_DIR/rootfs-x86_64-alpine.img" +CLAW_IMG="$ROOTFS_DIR/rootfs-x86_64-claw-code.img" +rm -f "$CLAW_IMG" +cp "$ALPINE_IMG" "$CLAW_IMG" + +echo "=== 3. Inject claw into rootfs ===" +inject_claw() { + local img="$1" + echo " injecting into $img ..." + debugfs -w "$img" -R "rm /usr/bin/claw" 2>/dev/null || true + debugfs -w "$img" -R "write $CLAW_BIN /usr/bin/claw" + debugfs -w "$img" -R "sif /usr/bin/claw mode 0100755" +} +inject_claw "$ALPINE_IMG" +inject_claw "$CLAW_IMG" + +echo "Injected claw into rootfs" + +# Place a marker so the overlay is never empty (app framework requires it). +mkdir -p "$OVERLAY" +touch "${OVERLAY}/.claw-injected" diff --git a/apps/starry/claw-code/qemu-x86_64.toml b/apps/starry/claw-code/qemu-x86_64.toml new file mode 100644 index 0000000000..82ac18cc20 --- /dev/null +++ b/apps/starry/claw-code/qemu-x86_64.toml @@ -0,0 +1,35 @@ +timeout = 0 +shell_prefix = "root@starry:" +uefi = false +to_bin = false +success_regex = [] +fail_regex = [] + +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-x86_64-claw-code.img,file.locking=off", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", + "-snapshot", +] + +shell_init_cmd = """ +export HOME=/root +export USER=root +export SHELL=/bin/sh +export TERM=xterm-256color +export PATH=/usr/local/bin:/usr/bin:/bin:/sbin +cat <<'STARRY_CLAW_USAGE' + +StarryOS Claw interactive shell is ready. + +常用命令: + claw + +退出 QEMU: + Ctrl-a x + +STARRY_CLAW_USAGE +""" diff --git a/os/StarryOS/axnsproxy/src/user.rs b/os/StarryOS/axnsproxy/src/user.rs index dd867ba669..1fc34012db 100644 --- a/os/StarryOS/axnsproxy/src/user.rs +++ b/os/StarryOS/axnsproxy/src/user.rs @@ -14,16 +14,24 @@ pub static ROOT_USER_NS: spin::LazyLock>> = /// `owner_uid` is the effective UID of the process that created the /// namespace (0 for the root namespace). /// -/// When `is_root` is false (child namespace without configured mappings), -/// all UIDs/GIDs are reported as `nobody` (65534), matching Linux default -/// behaviour for unconfigured user namespaces. +/// When neither `uid_mapped` nor `gid_mapped` is true (child namespace +/// without configured mappings), all UIDs/GIDs are reported as `nobody` +/// (65534), matching Linux default behaviour for unconfigured user +/// namespaces. Writing uid_map sets `uid_mapped`, writing gid_map sets +/// `gid_mapped`, so the two sides are independent — a half-configured +/// namespace correctly returns 65534 for the unmapped side. pub struct UserNamespace { /// Effective UID of the namespace creator (0 for root namespace). pub owner_uid: u32, - /// Whether this is the initial root user namespace. false for - /// namespaces created via unshare / clone(CLONE_NEWUSER) that - /// have not yet had uid_map / gid_map configured. + /// Whether this is the initial root user namespace or a child + /// namespace that had both uid_map and gid_map configured. Kept + /// for compatibility; new code should use `uid_mapped` / + /// `gid_mapped` for per-side overflow control. pub is_root: bool, + /// Whether uid_map has been written — lifts UID-side overflow. + pub uid_mapped: bool, + /// Whether gid_map has been written — lifts GID-side overflow. + pub gid_mapped: bool, } impl UserNamespace { @@ -31,6 +39,8 @@ impl UserNamespace { Self { owner_uid: 0, is_root: true, + uid_mapped: true, + gid_mapped: true, } } @@ -38,6 +48,8 @@ impl UserNamespace { Self { owner_uid: self.owner_uid, is_root: false, + uid_mapped: false, + gid_mapped: false, } } } diff --git a/os/StarryOS/kernel/src/pseudofs/proc.rs b/os/StarryOS/kernel/src/pseudofs/proc.rs index 155da91801..b3320e1102 100644 --- a/os/StarryOS/kernel/src/pseudofs/proc.rs +++ b/os/StarryOS/kernel/src/pseudofs/proc.rs @@ -36,7 +36,7 @@ use crate::{ SimpleFileOperation, SimpleFs, SpecialFsFile, }, task::{ - AsThread, ProcessData, TaskStat, get_process_data, get_task, processes, tasks, + AsThread, ProcessData, TaskStat, Thread, get_process_data, get_task, processes, tasks, tick_cpu_time, }, }; @@ -762,6 +762,10 @@ impl SimpleDirOps for ThreadDir { "comm", "exe", "fd", + "uid_map", + "gid_map", + "setgroups", + "cgroup", "ns", ] .into_iter() @@ -901,6 +905,131 @@ impl SimpleDirOps for ThreadDir { }), ) .into(), + "uid_map" => SimpleFile::new_regular( + fs, + RwFile::new(move |req| match req { + SimpleFileOperation::Read => { + let thr = task.as_thread(); + let cred = thr.cred(); + let content = if thr.uid_map_written() || cred.euid != 65534 { + format!(" 0 {:>10} 4294967295\n", cred.uid) + } else { + "\n".to_string() + }; + Ok(Some(content.into_bytes())) + } + SimpleFileOperation::Write(data) => { + let input = + core::str::from_utf8(data).map_err(|_| VfsError::InvalidInput)?; + // Linux uid_map format: + // Maps UIDs in the parent namespace (lower_uid..lower_uid+count) + // to UIDs in this namespace (upper_uid..upper_uid+count). + // + // StarryOS simplified semantics: we do not maintain namespace + // UID mappings; instead we directly set the thread's credentials + // to the upper_uid value (the UID this namespace wants to see). + // For the common `0 0 1` case (map root to root) this is correct. + // For non-trivial mappings this is an intentional simplification + // — StarryOS does not implement full user namespacing. + let parts: Vec<&str> = input.split_whitespace().collect(); + if parts.len() >= 3 { + let _mapped: u32 = + parts[0].parse().map_err(|_| VfsError::InvalidInput)?; + let orig: u32 = parts[1].parse().map_err(|_| VfsError::InvalidInput)?; + let _count: u32 = + parts[2].parse().map_err(|_| VfsError::InvalidInput)?; + let thr = task.as_thread(); + let mut cred = (*thr.cred()).clone(); + cred.uid = orig; + cred.euid = orig; + cred.suid = orig; + cred.fsuid = orig; + Thread::set_cred(thr, cred); + thr.set_uid_map_written(true); + // Mark the user namespace as UID-mapped so + // getuid/geteuid/getresuid return the mapped + // value instead of 65534 (nobody). + let proc_data = &thr.proc_data; + let nsproxy = proc_data.nsproxy.lock(); + nsproxy.user_ns.lock().uid_mapped = true; + } + Ok(None) + } + }), + ) + .into(), + "gid_map" => SimpleFile::new_regular( + fs, + RwFile::new(move |req| match req { + SimpleFileOperation::Read => { + let thr = task.as_thread(); + let cred = thr.cred(); + let content = if thr.gid_map_written() || cred.egid != 65534 { + format!(" 0 {:>10} 4294967295\n", cred.gid) + } else { + "\n".to_string() + }; + Ok(Some(content.into_bytes())) + } + SimpleFileOperation::Write(data) => { + let input = + core::str::from_utf8(data).map_err(|_| VfsError::InvalidInput)?; + // Linux gid_map format: + // Same simplified semantics as uid_map above. + // + // StarryOS does not maintain namespace GID mappings; + // it directly sets the thread's credentials to upper_gid. + let parts: Vec<&str> = input.split_whitespace().collect(); + if parts.len() >= 3 { + let _mapped: u32 = + parts[0].parse().map_err(|_| VfsError::InvalidInput)?; + let orig: u32 = parts[1].parse().map_err(|_| VfsError::InvalidInput)?; + let _count: u32 = + parts[2].parse().map_err(|_| VfsError::InvalidInput)?; + let thr = task.as_thread(); + let mut cred = (*thr.cred()).clone(); + cred.gid = orig; + cred.egid = orig; + cred.sgid = orig; + cred.fsgid = orig; + Thread::set_cred(thr, cred); + thr.set_gid_map_written(true); + let proc_data = &thr.proc_data; + let nsproxy = proc_data.nsproxy.lock(); + nsproxy.user_ns.lock().gid_mapped = true; + } + Ok(None) + } + }), + ) + .into(), + "setgroups" => SimpleFile::new_regular( + fs, + RwFile::new(move |req| match req { + SimpleFileOperation::Read => { + let thr = task.as_thread(); + let content = if thr.setgroups_deny() { + "deny\n" + } else { + "allow\n" + }; + Ok(Some(content.as_bytes().to_vec())) + } + SimpleFileOperation::Write(data) => { + let input = core::str::from_utf8(data) + .map_err(|_| VfsError::InvalidInput)? + .trim(); + if input == "deny" { + task.as_thread().set_setgroups_deny(true); + } else if input == "allow" { + task.as_thread().set_setgroups_deny(false); + } + Ok(None) + } + }), + ) + .into(), + "cgroup" => SimpleFile::new_regular(fs, move || Ok("0::/\n")).into(), "ns" => SimpleDir::new_maker( fs.clone(), Arc::new(NsDir { diff --git a/os/StarryOS/kernel/src/syscall/fs/mount.rs b/os/StarryOS/kernel/src/syscall/fs/mount.rs index a70a575e93..d3b83bb36a 100644 --- a/os/StarryOS/kernel/src/syscall/fs/mount.rs +++ b/os/StarryOS/kernel/src/syscall/fs/mount.rs @@ -1,4 +1,4 @@ -use alloc::sync::Arc; +use alloc::{string::String, sync::Arc}; use core::ffi::{c_char, c_void}; use ax_errno::{AxError, AxResult, LinuxError}; @@ -70,7 +70,11 @@ pub fn sys_mount( ) -> AxResult { let source = vm_load_string(source)?; let target = vm_load_string(target)?; - let fs_type = vm_load_string(fs_type)?; + let fs_type = if fs_type.is_null() { + String::new() + } else { + vm_load_string(fs_type)? + }; debug!("sys_mount <= source: {source:?}, target: {target:?}, fs_type: {fs_type:?}"); let propagation = flags & PROPAGATION_FLAGS; diff --git a/os/StarryOS/kernel/src/syscall/sys.rs b/os/StarryOS/kernel/src/syscall/sys.rs index 9a254c0eb7..c86b4caeb7 100644 --- a/os/StarryOS/kernel/src/syscall/sys.rs +++ b/os/StarryOS/kernel/src/syscall/sys.rs @@ -118,14 +118,21 @@ fn dumpable_should_reset(old: &crate::task::Cred, new: &crate::task::Cred) -> bo old.euid != new.euid || old.egid != new.egid || old.fsuid != new.fsuid || old.fsgid != new.fsgid } -fn user_ns_is_root() -> bool { +fn user_ns_overflow_uid() -> u32 { let curr = current(); let nsproxy = curr.as_thread().proc_data.nsproxy.lock(); - nsproxy.user_ns.lock().is_root + let ns = nsproxy.user_ns.lock(); + if ns.is_root || ns.uid_mapped { + return 0; + } + 65534 } -fn user_ns_overflow_uid() -> u32 { - if user_ns_is_root() { +fn user_ns_overflow_gid() -> u32 { + let curr = current(); + let nsproxy = curr.as_thread().proc_data.nsproxy.lock(); + let ns = nsproxy.user_ns.lock(); + if ns.is_root || ns.gid_mapped { return 0; } 65534 @@ -150,7 +157,7 @@ pub fn sys_geteuid() -> AxResult { } pub fn sys_getgid() -> AxResult { - let overflow = user_ns_overflow_uid(); + let overflow = user_ns_overflow_gid(); if overflow != 0 { return Ok(overflow as isize); } @@ -159,7 +166,7 @@ pub fn sys_getgid() -> AxResult { } pub fn sys_getegid() -> AxResult { - let overflow = user_ns_overflow_uid(); + let overflow = user_ns_overflow_gid(); if overflow != 0 { return Ok(overflow as isize); } @@ -183,7 +190,7 @@ pub fn sys_getresuid(ruid: *mut u32, euid: *mut u32, suid: *mut u32) -> AxResult } pub fn sys_getresgid(rgid: *mut u32, egid: *mut u32, sgid: *mut u32) -> AxResult { - let overflow = user_ns_overflow_uid(); + let overflow = user_ns_overflow_gid(); if overflow != 0 { rgid.vm_write(overflow)?; egid.vm_write(overflow)?; @@ -560,6 +567,10 @@ pub fn sys_setgroups(size: usize, list: *const u32) -> AxResult { if !old.has_cap_setgid() { return Err(AxError::OperationNotPermitted); } + // Linux 3.19+: writing "deny" to /proc/self/setgroups prevents setgroups(2). + if thread.setgroups_deny() { + return Err(AxError::OperationNotPermitted); + } if size > NGROUPS_MAX { return Err(AxError::InvalidInput); } diff --git a/os/StarryOS/kernel/src/task/mod.rs b/os/StarryOS/kernel/src/task/mod.rs index 7b354cdea5..38cedf8f09 100644 --- a/os/StarryOS/kernel/src/task/mod.rs +++ b/os/StarryOS/kernel/src/task/mod.rs @@ -170,6 +170,15 @@ pub struct Thread { pub fault_dump_signo: AtomicU8, pub kretprobe_stack: SpinNoIrq>, + + /// Whether uid_map has been written for this thread's user namespace. + uid_map_written: AtomicBool, + + /// Whether gid_map has been written for this thread's user namespace. + gid_map_written: AtomicBool, + + /// Whether setgroups has been set to "deny" for this thread's user namespace. + setgroups_deny: AtomicBool, } impl Thread { @@ -200,6 +209,10 @@ impl Thread { fault_dump_signo: AtomicU8::new(0), kretprobe_stack: SpinNoIrq::new(alloc::vec::Vec::new()), + + uid_map_written: AtomicBool::new(false), + gid_map_written: AtomicBool::new(false), + setgroups_deny: AtomicBool::new(false), }) } @@ -359,6 +372,36 @@ impl Thread { self.rseq_signature.load(Ordering::SeqCst) } + /// Check if uid_map has been written for this thread's user namespace. + pub fn uid_map_written(&self) -> bool { + self.uid_map_written.load(Ordering::Relaxed) + } + + /// Mark uid_map as written. + pub fn set_uid_map_written(&self, val: bool) { + self.uid_map_written.store(val, Ordering::Relaxed); + } + + /// Check if gid_map has been written for this thread's user namespace. + pub fn gid_map_written(&self) -> bool { + self.gid_map_written.load(Ordering::Relaxed) + } + + /// Mark gid_map as written. + pub fn set_gid_map_written(&self, val: bool) { + self.gid_map_written.store(val, Ordering::Relaxed); + } + + /// Check if setgroups has been set to "deny". + pub fn setgroups_deny(&self) -> bool { + self.setgroups_deny.load(Ordering::Relaxed) + } + + /// Set the setgroups deny flag. + pub fn set_setgroups_deny(&self, val: bool) { + self.setgroups_deny.store(val, Ordering::Relaxed); + } + /// Set the registered rseq area pointer. pub fn set_rseq_area(&self, addr: usize) { self.rseq_area.store(addr, Ordering::SeqCst); diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/build-claw.sh b/test-suit/starryos/normal/qemu-smp1/claw-code/build-claw.sh new file mode 100755 index 0000000000..65a006427e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/build-claw.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Build claw from source and cache the binary. +# Called by build.rs files; idempotent — builds only once. +set -euo pipefail + +CLAW_REPO="https://github.com/MuZhao2333/claw-code" +CACHE_DIR="${CLAW_CACHE_DIR:-${HOME}/.cache/claw-code-build}" +CLAW_SRC="$CACHE_DIR/repo" +# Statically-linked musl binary so it runs inside Alpine-based StarryOS rootfs. +TARGET="x86_64-unknown-linux-musl" +CLAW_BIN="$CACHE_DIR/claw" + +if [ -f "$CLAW_BIN" ]; then + echo "claw binary cached at $CLAW_BIN" + echo -n "$CLAW_BIN" + exit 0 +fi + +echo "=== Building claw from source (target: $TARGET) ===" +mkdir -p "$CACHE_DIR" + +# Install musl target if needed. +rustup target add "$TARGET" 2>/dev/null || true + +if [ ! -d "$CLAW_SRC" ]; then + echo "Cloning $CLAW_REPO ..." + git clone --depth 1 "$CLAW_REPO" "$CLAW_SRC" +fi + +echo "Building claw (this may take a while)..." +( + cd "$CLAW_SRC/rust" + cargo build --workspace --release --target "$TARGET" --target-dir "$CACHE_DIR/target" +) + +cp "$CACHE_DIR/target/$TARGET/release/claw" "$CLAW_BIN" +chmod +x "$CLAW_BIN" +echo "claw binary built: $CLAW_BIN" +echo -n "$CLAW_BIN" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/CMakeLists.txt new file mode 100644 index 0000000000..6ee38cbb27 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-claw-code-goal-01-unshare C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-claw-code-goal-01-unshare src/main.c) +target_include_directories(test-claw-code-goal-01-unshare PRIVATE src) +target_compile_options(test-claw-code-goal-01-unshare PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-claw-code-goal-01-unshare RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/src/main.c new file mode 100644 index 0000000000..33a6378df9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/src/main.c @@ -0,0 +1,54 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include + +/* + * unshare(2) syscall test + * + * int unshare(int flags); + * + * Key semantics: + * 1. CLONE_NEWUSER creates a new user namespace + * 2. Returns 0 on success, -1 on error + * 3. EINVAL for invalid/unsupported flags + * 4. EPERM when caller lacks CAP_SYS_ADMIN (for non-user namespaces) + * 5. After CLONE_NEWUSER, uid/gid in new namespace start as 65534 (nobody) + */ + +static int call_unshare(int flags) +{ + errno = 0; + return syscall(SYS_unshare, flags); +} + +int main(void) +{ + TEST_START("unshare"); + + /* 1. unshare(CLONE_NEWUSER) — create new user namespace */ + { + int ret = call_unshare(CLONE_NEWUSER); + CHECK(ret == 0, "unshare(CLONE_NEWUSER) should return 0"); + + /* After unshare(CLONE_NEWUSER), uid in new namespace is 65534 */ + uid_t uid = getuid(); + CHECK(uid == 65534, "after unshare(NEWUSER), getuid() should be 65534 (nobody)"); + } + + /* 2. unshare(0) — flags=0 is valid and a no-op */ + { + int ret = call_unshare(0); + CHECK(ret == 0, "unshare(0) should return 0 (no-op)"); + } + + /* 3. unshare with invalid flag (0xdeadbeef) — should get EINVAL */ + { + int ret = call_unshare(0xdeadbeef); + CHECK(ret == -1 && errno == EINVAL, + "unshare(0xdeadbeef) should fail with EINVAL"); + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/src/test_framework.h b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/c/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/qemu-riscv64.toml new file mode 100644 index 0000000000..5bb9fb304d --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/qemu-riscv64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-01-unshare" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/qemu-x86_64.toml new file mode 100644 index 0000000000..99b9540a03 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-01-unshare/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-01-unshare" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/CMakeLists.txt new file mode 100644 index 0000000000..1f4e07e8ba --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-claw-code-goal-02-uid-map C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-claw-code-goal-02-uid-map src/main.c) +target_include_directories(test-claw-code-goal-02-uid-map PRIVATE src) +target_compile_options(test-claw-code-goal-02-uid-map PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-claw-code-goal-02-uid-map RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/src/main.c new file mode 100644 index 0000000000..7446ab2f1c --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/src/main.c @@ -0,0 +1,105 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +/* + * /proc/self/uid_map test + * + * After unshare(CLONE_NEWUSER): + * 1. /proc/self/uid_map should exist and be readable + * 2. Writing "0 0 1" maps UID 0 + * 3. getuid()/geteuid()/getresuid() return the mapped value (0) + * 4. Writing uid_map must NOT affect GID side (getgid() stays 65534) + */ + +static int call_unshare(int flags) { + errno = 0; + return syscall(SYS_unshare, flags); +} + +int main(void) { + TEST_START("uid_map"); + + /* 1. /proc/self/uid_map should exist (no unshare needed) */ + { + int fd = open("/proc/self/uid_map", O_RDONLY); + CHECK(fd >= 0, "/proc/self/uid_map should exist and be readable"); + if (fd >= 0) { + char buf[256] = {0}; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + CHECK(n >= 0, "read /proc/self/uid_map should succeed"); + close(fd); + } + } + + /* 2. unshare(CLONE_NEWUSER) then write uid_map + assert getuid/getresuid */ + { + int ret = call_unshare(CLONE_NEWUSER); + CHECK(ret == 0, "unshare(CLONE_NEWUSER) should return 0"); + + if (ret == 0) { + /* Before writing uid_map, getuid() should return 65534 (nobody) */ + uid_t uid_before = getuid(); + printf(" INFO | uid before map write: %u\n", uid_before); + CHECK(uid_before == 65534, + "before writing uid_map, getuid() should return 65534 (nobody)"); + + /* GID side should also be 65534 */ + gid_t gid_before = getgid(); + printf(" INFO | gid before uid_map write: %u\n", gid_before); + CHECK(gid_before == 65534, + "before writing uid_map, getgid() should return 65534 (nobody)"); + + /* Write "0 0 1" to map namespace UID 0 to UID 0 */ + int fd = open("/proc/self/uid_map", O_WRONLY); + CHECK(fd >= 0, "/proc/self/uid_map should be writable after unshare"); + if (fd >= 0) { + const char *mapping = "0 0 1\n"; + ssize_t n = write(fd, mapping, strlen(mapping)); + CHECK(n > 0, "write to uid_map should succeed after unshare"); + close(fd); + } + + /* After writing uid_map, getuid() should return 0 */ + uid_t uid_after = getuid(); + printf(" INFO | uid after map write: %u\n", uid_after); + CHECK(uid_after == 0, + "after writing uid_map \"0 0 1\", getuid() should return 0"); + + /* geteuid() should also return 0 */ + uid_t euid_after = geteuid(); + CHECK(euid_after == 0, + "after writing uid_map, geteuid() should return 0"); + + /* getresuid() should return 0 for all three */ + uid_t ruid, reuid, rsuid; + CHECK_RET(getresuid(&ruid, &reuid, &rsuid), 0, "getresuid should succeed"); + CHECK(ruid == 0 && reuid == 0 && rsuid == 0, + "after writing uid_map, getresuid() should return (0,0,0)"); + + /* GID side must NOT be affected by uid_map write */ + gid_t gid_after = getgid(); + printf(" INFO | gid after uid_map write: %u\n", gid_after); + CHECK(gid_after == 65534, + "after writing uid_map, getgid() should STILL return 65534 (not affected by uid_map)"); + + /* getegid() should also still return 65534 */ + gid_t egid_after = getegid(); + CHECK(egid_after == 65534, + "after writing uid_map, getegid() should STILL return 65534"); + + /* getresgid() should still return 65534 for all three */ + gid_t rgid, regid, rsgid; + CHECK_RET(getresgid(&rgid, ®id, &rsgid), 0, "getresgid should succeed"); + CHECK(rgid == 65534 && regid == 65534 && rsgid == 65534, + "after writing uid_map, getresgid() should return (65534,65534,65534)"); + } + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/src/test_framework.h b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/c/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/qemu-riscv64.toml new file mode 100644 index 0000000000..c63c27269a --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/qemu-riscv64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-02-uid-map" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/qemu-x86_64.toml new file mode 100644 index 0000000000..316d9d5a26 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-02-uid-map/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-02-uid-map" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/CMakeLists.txt new file mode 100644 index 0000000000..cbe6b4c432 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-claw-code-goal-03-gid-map C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-claw-code-goal-03-gid-map src/main.c) +target_include_directories(test-claw-code-goal-03-gid-map PRIVATE src) +target_compile_options(test-claw-code-goal-03-gid-map PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-claw-code-goal-03-gid-map RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/src/main.c new file mode 100644 index 0000000000..87636683b9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/src/main.c @@ -0,0 +1,105 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +/* + * /proc/self/gid_map test + * + * After unshare(CLONE_NEWUSER): + * 1. /proc/self/gid_map should exist and be readable + * 2. Writing "0 0 1" maps GID 0 + * 3. getgid()/getegid()/getresgid() return the mapped value (0) + * 4. Writing gid_map must NOT affect UID side (getuid() stays 65534) + */ + +static int call_unshare(int flags) { + errno = 0; + return syscall(SYS_unshare, flags); +} + +int main(void) { + TEST_START("gid_map"); + + /* 1. /proc/self/gid_map should exist (no unshare needed) */ + { + int fd = open("/proc/self/gid_map", O_RDONLY); + CHECK(fd >= 0, "/proc/self/gid_map should exist and be readable"); + if (fd >= 0) { + char buf[256] = {0}; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + CHECK(n >= 0, "read /proc/self/gid_map should succeed"); + close(fd); + } + } + + /* 2. unshare(CLONE_NEWUSER) then write gid_map + assert getgid/getresgid */ + { + int ret = call_unshare(CLONE_NEWUSER); + CHECK(ret == 0, "unshare(CLONE_NEWUSER) should return 0"); + + if (ret == 0) { + /* Before writing gid_map, getgid() should return 65534 (nobody) */ + gid_t gid_before = getgid(); + printf(" INFO | gid before map write: %u\n", gid_before); + CHECK(gid_before == 65534, + "before writing gid_map, getgid() should return 65534 (nobody)"); + + /* UID side should also be 65534 */ + uid_t uid_before = getuid(); + printf(" INFO | uid before gid_map write: %u\n", uid_before); + CHECK(uid_before == 65534, + "before writing gid_map, getuid() should return 65534 (nobody)"); + + /* Write "0 0 1" to map namespace GID 0 to GID 0 */ + int fd = open("/proc/self/gid_map", O_WRONLY); + CHECK(fd >= 0, "/proc/self/gid_map should be writable after unshare"); + if (fd >= 0) { + const char *mapping = "0 0 1\n"; + ssize_t n = write(fd, mapping, strlen(mapping)); + CHECK(n > 0, "write to gid_map should succeed after unshare"); + close(fd); + } + + /* After writing gid_map, getgid() should return 0 */ + gid_t gid_after = getgid(); + printf(" INFO | gid after map write: %u\n", gid_after); + CHECK(gid_after == 0, + "after writing gid_map \"0 0 1\", getgid() should return 0"); + + /* getegid() should also return 0 */ + gid_t egid_after = getegid(); + CHECK(egid_after == 0, + "after writing gid_map, getegid() should return 0"); + + /* getresgid() should return 0 for all three */ + gid_t rgid, regid, rsgid; + CHECK_RET(getresgid(&rgid, ®id, &rsgid), 0, "getresgid should succeed"); + CHECK(rgid == 0 && regid == 0 && rsgid == 0, + "after writing gid_map, getresgid() should return (0,0,0)"); + + /* UID side must NOT be affected by gid_map write */ + uid_t uid_after = getuid(); + printf(" INFO | uid after gid_map write: %u\n", uid_after); + CHECK(uid_after == 65534, + "after writing gid_map, getuid() should STILL return 65534 (not affected by gid_map)"); + + /* geteuid() should also still return 65534 */ + uid_t euid_after = geteuid(); + CHECK(euid_after == 65534, + "after writing gid_map, geteuid() should STILL return 65534"); + + /* getresuid() should still return 65534 for all three */ + uid_t ruid, reuid, rsuid; + CHECK_RET(getresuid(&ruid, &reuid, &rsuid), 0, "getresuid should succeed"); + CHECK(ruid == 65534 && reuid == 65534 && rsuid == 65534, + "after writing gid_map, getresuid() should return (65534,65534,65534)"); + } + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/src/test_framework.h b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/c/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/qemu-riscv64.toml new file mode 100644 index 0000000000..3af88f07be --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/qemu-riscv64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-03-gid-map" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/qemu-x86_64.toml new file mode 100644 index 0000000000..1ebd240d91 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-03-gid-map/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-03-gid-map" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/CMakeLists.txt new file mode 100644 index 0000000000..eb12f05200 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-claw-code-goal-04-setgroups C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-claw-code-goal-04-setgroups src/main.c) +target_include_directories(test-claw-code-goal-04-setgroups PRIVATE src) +target_compile_options(test-claw-code-goal-04-setgroups PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-claw-code-goal-04-setgroups RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/src/main.c new file mode 100644 index 0000000000..8b34400d96 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/src/main.c @@ -0,0 +1,122 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +/* + * /proc/self/setgroups test + * + * 1. /proc/self/setgroups should exist and be readable + * 2. Reading returns "allow" by default + * 3. Writing "deny" changes the value and causes setgroups(2) to return EPERM + * 4. Writing "allow" restores original behaviour + * 5. After unshare(CLONE_NEWUSER), setgroups should still exist + */ + +static int call_unshare(int flags) { + errno = 0; + return syscall(SYS_unshare, flags); +} + +int main(void) { + TEST_START("setgroups"); + + /* 1. /proc/self/setgroups should exist */ + { + int fd = open("/proc/self/setgroups", O_RDONLY); + CHECK(fd >= 0, "/proc/self/setgroups should exist and be readable"); + if (fd >= 0) { + char buf[32] = {0}; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + CHECK(n >= 0, "read /proc/self/setgroups should succeed"); + if (n >= 0) { + printf(" INFO | setgroups content: %.*s\n", (int)n, buf); + } + close(fd); + } + } + + /* 2. Write "deny" to setgroups and verify setgroups(2) returns EPERM */ + { + int fd = open("/proc/self/setgroups", O_WRONLY); + if (fd >= 0) { + const char *val = "deny\n"; // include newline to replace full content + ssize_t n = write(fd, val, strlen(val)); + CHECK(n > 0, "write \"deny\" to setgroups should succeed"); + close(fd); + + if (n > 0) { + /* Verify read-back: content should contain "deny" */ + fd = open("/proc/self/setgroups", O_RDONLY); + if (fd >= 0) { + char buf[32] = {0}; + ssize_t rn = read(fd, buf, sizeof(buf) - 1); + CHECK(rn >= 0, "read setgroups after deny should succeed"); + if (rn >= 0) { + printf(" INFO | setgroups read-back (%zd bytes): \"%.*s\"\n", + rn, (int)rn, buf); + int has_deny = (rn >= 4 && memcmp(buf, "deny", 4) == 0); + CHECK(has_deny, "reading setgroups after deny should return \"deny\""); + } + close(fd); + } + + /* setgroups(2) must now return EPERM */ + errno = 0; + long after_ret = syscall(SYS_setgroups, 0, NULL); + int after_errno = errno; + printf(" INFO | setgroups(0, NULL) after deny: ret=%ld errno=%d\n", + after_ret, after_errno); + CHECK(after_ret == -1 && after_errno == EPERM, + "after writing \"deny\", setgroups(2) should return EPERM"); + } + } else { + printf(" PASS | %s:%d | setgroups not writable (errno=%d)\n", + __FILE__, __LINE__, errno); + __pass++; + } + } + + /* 3. Write "allow" back and verify setgroups works again */ + { + int fd = open("/proc/self/setgroups", O_WRONLY); + if (fd >= 0) { + const char *val = "allow\n"; // include newline to replace full content + ssize_t n = write(fd, val, strlen(val)); + CHECK(n > 0, "write \"allow\" to setgroups should succeed"); + close(fd); + + if (n > 0) { + errno = 0; + long ret = syscall(SYS_setgroups, 0, NULL); + int final_errno = errno; + printf(" INFO | setgroups(0, NULL) after allow: ret=%ld errno=%d\n", + ret, final_errno); + CHECK(final_errno != EPERM, + "after writing \"allow\", setgroups(2) should NOT return EPERM"); + } + } + } + + /* 4. After unshare(CLONE_NEWUSER), setgroups should still exist */ + { + int ret = call_unshare(CLONE_NEWUSER); + CHECK(ret == 0, "unshare(CLONE_NEWUSER) should return 0"); + if (ret == 0) { + int fd = open("/proc/self/setgroups", O_RDONLY); + CHECK(fd >= 0, "/proc/self/setgroups should exist after unshare"); + if (fd >= 0) { + char buf[32] = {0}; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + CHECK(n >= 0, "read setgroups after unshare should succeed"); + close(fd); + } + } + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/src/test_framework.h b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/c/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/qemu-riscv64.toml new file mode 100644 index 0000000000..ff57a9c777 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/qemu-riscv64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-04-setgroups" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/qemu-x86_64.toml new file mode 100644 index 0000000000..ccfd60b140 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-04-setgroups/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-04-setgroups" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/CMakeLists.txt b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/CMakeLists.txt new file mode 100644 index 0000000000..1eccd8ab6b --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-claw-code-goal-05-cgroup C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-claw-code-goal-05-cgroup src/main.c) +target_include_directories(test-claw-code-goal-05-cgroup PRIVATE src) +target_compile_options(test-claw-code-goal-05-cgroup PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-claw-code-goal-05-cgroup RUNTIME DESTINATION usr/bin) diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/src/main.c new file mode 100644 index 0000000000..d31076c556 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/src/main.c @@ -0,0 +1,49 @@ +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include + +/* + * /proc/[pid]/cgroup test + * + * 1. /proc/1/cgroup should exist and be readable + * 2. /proc/self/cgroup should exist and be readable + * 3. Content should be valid cgroup info + */ + +int main(void) { + TEST_START("cgroup"); + + /* 1. /proc/1/cgroup should exist and be readable */ + { + int fd = open("/proc/1/cgroup", O_RDONLY); + CHECK(fd >= 0, "/proc/1/cgroup should exist and be readable"); + if (fd >= 0) { + char buf[256] = {0}; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + CHECK(n >= 0, "read /proc/1/cgroup should succeed"); + if (n > 0) { + printf(" INFO | /proc/1/cgroup: %.*s", (int)n, buf); + } + close(fd); + } + } + + /* 2. /proc/self/cgroup should exist */ + { + int fd = open("/proc/self/cgroup", O_RDONLY); + CHECK(fd >= 0, "/proc/self/cgroup should exist and be readable"); + if (fd >= 0) { + char buf[256] = {0}; + ssize_t n = read(fd, buf, sizeof(buf) - 1); + CHECK(n >= 0, "read /proc/self/cgroup should succeed"); + if (n > 0) { + printf(" INFO | /proc/self/cgroup: %.*s", (int)n, buf); + } + close(fd); + } + } + + TEST_DONE(); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/src/test_framework.h b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/c/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/qemu-riscv64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/qemu-riscv64.toml new file mode 100644 index 0000000000..1e12e3ef36 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/qemu-riscv64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", "-cpu", "rv64", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-riscv64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = true +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-05-cgroup" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/qemu-x86_64.toml new file mode 100644 index 0000000000..cce46a57da --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/goal-05-cgroup/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/test-claw-code-goal-05-cgroup" +success_regex = ["(?m)DONE: \\d+ pass, 0 fail"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', '(?m)FAIL'] +timeout = 120 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/integration/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/qemu-x86_64.toml new file mode 100644 index 0000000000..a6e2f5772f --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0", +] +uefi = false +to_bin = false +shell_init_cmd = "/usr/bin/claw-test" +shell_prefix = "root@starry:" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 1800 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/integration/run-local.sh b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/run-local.sh new file mode 100755 index 0000000000..6f79166d9b --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/run-local.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Build claw from source, inject into rootfs, and boot StarryOS. +# Usage: docker run --rm -v "$(pwd)":/workspace -w /workspace starryos-dev:ubuntu-qemu10.2.1 \ +# bash test-suit/starryos/normal/qemu-smp1/claw-code/integration/run-local.sh +set -eu + +BUILD_SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/build-claw.sh" +ROOTFS="/workspace/tmp/axbuild/rootfs/rootfs-x86_64-alpine.img" + +echo "=== 1. Build StarryOS (ensures rootfs exists) ===" +cargo xtask starry quick-start qemu-x86_64 build + +echo "=== 2. Build claw from source ===" +CLAW_BIN="$(bash "$BUILD_SCRIPT")" +echo "claw binary: $CLAW_BIN" + +echo "=== 3. Inject claw into rootfs ===" +debugfs -w "$ROOTFS" -R "rm /usr/bin/claw" 2>/dev/null || true +debugfs -w "$ROOTFS" -R "write $CLAW_BIN /usr/bin/claw" +debugfs -w "$ROOTFS" -R "sif /usr/bin/claw mode 0100755" +echo "Injected claw into rootfs" + +echo "=== 4. Boot StarryOS ===" +cargo xtask starry quick-start qemu-x86_64 run diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/Cargo.lock b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/Cargo.lock new file mode 100644 index 0000000000..dc0dfc73a6 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "claw-test" +version = "0.1.0" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/Cargo.toml new file mode 100644 index 0000000000..67dc60044b --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-test" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-test" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/src/main.rs new file mode 100644 index 0000000000..c079a9ac33 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/integration/rust/src/main.rs @@ -0,0 +1,126 @@ +use std::fs; +use std::io::Write; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; + +// --- Configuration --- +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + + + +fn main() { + // Extract embedded claw binary (provided by build.rs from local file or GitHub release) + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + let mut perms = fs::metadata(CLAW_PATH) + .expect("failed to stat claw") + .permissions(); + perms.set_mode(0o755); + fs::set_permissions(CLAW_PATH, perms).expect("failed to chmod claw"); + + run_test("/tmp/claw --help", "=== Smoke: claw --help ==="); + run_test("/tmp/claw version", "=== Diagnostic: claw version ==="); + + // Create fake git repo so claw sees a normal git environment + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + + // Functional: basic prompt + if !run_test_with_retry( + &format!("cd /tmp/work && {env} timeout 300 /tmp/claw prompt 'say just the word ok and nothing else' 2>&1"), + "=== Functional: claw prompt ===", + 3, + ) { + println!("ALL_TESTS_FAILED: functional"); + std::process::exit(1); + } + + // Tool test: bash echo + if !run_test_with_retry( + &format!("cd /tmp/work && {env} timeout 300 /tmp/claw --allowedTools bash prompt 'use bash to echo hello world' 2>&1"), + "=== Tool: bash echo ===", + 3, + ) { + println!("ALL_TESTS_FAILED: bash"); + std::process::exit(1); + } + + // Small project: create a file + if !run_test_with_retry( + &format!("cd /tmp/work && {env} timeout 300 /tmp/claw --allowedTools bash,write prompt 'create a file /tmp/claw-hello.txt containing exactly: hello from claw' 2>&1"), + "=== Project: create file ===", + 3, + ) { + println!("ALL_TESTS_FAILED: create file"); + std::process::exit(1); + } + run_test("cat /tmp/claw-hello.txt 2>&1", "=== Verify: cat /tmp/claw-hello.txt ==="); + + // C project: write, compile, and run a program + if !run_test_with_retry( + &format!("cd /tmp/work && {env} timeout 300 /tmp/claw --allowedTools bash,write prompt 'write a C program /tmp/hello.c that prints \"StarryOS CLAW OK\", compile it, and run it' 2>&1"), + "=== Project: C compile & run ===", + 3, + ) { + println!("ALL_TESTS_FAILED: C compile"); + std::process::exit(1); + } + run_test("cat /tmp/hello.c 2>&1", "=== Verify: cat /tmp/hello.c ==="); + + println!("ALL_TESTS_DONE"); +} + +fn run_test(cmd: &str, label: &str) { + println!("{label}"); + let output = Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("failed to execute test command"); + let status = output.status; + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + println!("EXIT:{}", status.code().unwrap_or(-1)); +} + +fn run_test_with_retry(cmd: &str, label: &str, max_retries: u32) -> bool { + for attempt in 1..=max_retries { + println!("{label} (attempt {}/{})", attempt, max_retries); + let output = Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("failed to execute test command"); + let status = output.status; + std::io::stdout().write_all(&output.stdout).unwrap(); + std::io::stderr().write_all(&output.stderr).unwrap(); + let code = status.code().unwrap_or(-1); + println!("EXIT:{}", code); + if code == 0 { + return true; + } + if attempt < max_retries { + println!("Retrying..."); + std::thread::sleep(std::time::Duration::from_secs(2)); + } + } + false +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/qemu-x86_64.toml new file mode 100644 index 0000000000..7487e2adc5 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-01" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 1800 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/Cargo.lock b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/Cargo.lock new file mode 100644 index 0000000000..0c2dab072e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "claw-robust-01" +version = "0.1.0" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/Cargo.toml new file mode 100644 index 0000000000..5d49e93765 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-01" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-01" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/src/main.rs new file mode 100644 index 0000000000..4c450270e0 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-01/rust/src/main.rs @@ -0,0 +1,57 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + + + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + fs::set_permissions(CLAW_PATH, fs::metadata(CLAW_PATH).unwrap().permissions()).ok(); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-01: search LLM agent papers, write digest ==="); + let (code, _out, _err) = run(&format!("cd /tmp/work && {env} timeout 600 /tmp/claw --allowedTools bash,write prompt 'search for llm agent papers and write a digest to digest.md' 2>&1")); + println!("EXIT:{code}"); + + if std::path::Path::new("/tmp/work/digest.md").exists() { + let content = fs::read_to_string("/tmp/work/digest.md").unwrap_or_default(); + println!("digest.md exists with {} lines", content.lines().count()); + println!("ALL_TESTS_DONE"); + } else { + println!("digest.md NOT found"); + println!("ALL_TESTS_FAILED"); + std::process::exit(1); + } +} + +fn run(cmd: &str) -> (i32, String, String) { + let output = Command::new("sh").arg("-c").arg(cmd).output().expect("command failed"); + let code = output.status.code().unwrap_or(-1); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + print!("{stdout}"); + eprint!("{stderr}"); + (code, stdout, stderr) +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/qemu-x86_64.toml new file mode 100644 index 0000000000..61c1f52458 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-02" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 300 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/Cargo.toml new file mode 100644 index 0000000000..61f680692d --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-02" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-02" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/src/main.rs new file mode 100644 index 0000000000..8889ca212e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-02/rust/src/main.rs @@ -0,0 +1,44 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-02: count files recursively, write count ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 300 /tmp/claw --allowedTools bash,write prompt 'use bash to count all files recursively in /tmp, and write the number to count.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + eprintln!("{}", String::from_utf8_lossy(&output.stderr)); + + if std::path::Path::new("/tmp/work/count.txt").exists() { + let s = fs::read_to_string("/tmp/work/count.txt").unwrap_or_default(); + println!("count.txt exists: {s}"); + println!("ALL_TESTS_DONE"); + } else { + println!("ALL_TESTS_FAILED"); + std::process::exit(1); + } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/qemu-x86_64.toml new file mode 100644 index 0000000000..adc3950a30 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-03" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 600 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/Cargo.toml new file mode 100644 index 0000000000..250df712cf --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-03" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-03" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/src/main.rs new file mode 100644 index 0000000000..390a11e3da --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-03/rust/src/main.rs @@ -0,0 +1,41 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-03: print numbers 1-100 with squares ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 600 /tmp/claw --allowedTools bash,write prompt 'write a shell script that prints numbers 1 to 100 with their squares, run it, and save the output to squares.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/squares.txt").exists() { + let s = fs::read_to_string("/tmp/work/squares.txt").unwrap_or_default(); + let last = s.lines().last().unwrap_or(""); + println!("squares.txt last line: {last}"); + if last.contains("10000") { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/qemu-x86_64.toml new file mode 100644 index 0000000000..23f9ca9ee7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-04" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 600 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/Cargo.toml new file mode 100644 index 0000000000..214e908101 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-04" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-04" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/src/main.rs new file mode 100644 index 0000000000..c5dec1687c --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-04/rust/src/main.rs @@ -0,0 +1,40 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-04: date and /etc listing report ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 600 /tmp/claw --allowedTools bash,write prompt 'create a markdown report with the current date and a listing of files in /etc, save to report.md' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/report.md").exists() { + let s = fs::read_to_string("/tmp/work/report.md").unwrap_or_default(); + println!("report.md: {} lines", s.lines().count()); + if s.to_lowercase().contains("date") || s.contains("20") { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/qemu-x86_64.toml new file mode 100644 index 0000000000..f30d325216 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-05" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 600 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/Cargo.toml new file mode 100644 index 0000000000..74111bd089 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-05" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-05" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/src/main.rs new file mode 100644 index 0000000000..c804a37202 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-05/rust/src/main.rs @@ -0,0 +1,41 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-05: generate first 50 prime numbers ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 600 /tmp/claw --allowedTools bash,write prompt 'write a Python script that generates the first 50 prime numbers, run it, and save the output to primes.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/primes.txt").exists() { + let s = fs::read_to_string("/tmp/work/primes.txt").unwrap_or_default(); + let last = s.lines().last().unwrap_or(""); + println!("primes.txt last: {last}"); + if last.contains("229") { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/qemu-x86_64.toml new file mode 100644 index 0000000000..1b4b5aac0a --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-06" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 600 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/Cargo.toml new file mode 100644 index 0000000000..7c409bd7d9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-06" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-06" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/src/main.rs new file mode 100644 index 0000000000..29ef8637e3 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-06/rust/src/main.rs @@ -0,0 +1,40 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-06: find and sort directories ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 600 /tmp/claw --allowedTools bash,write prompt 'use bash to find all directories in /, sort them alphabetically, and write the first 10 to topdirs.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/topdirs.txt").exists() { + let s = fs::read_to_string("/tmp/work/topdirs.txt").unwrap_or_default(); + println!("topdirs.txt: {} lines", s.lines().count()); + println!("ALL_TESTS_DONE"); + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/qemu-x86_64.toml new file mode 100644 index 0000000000..7e9855eab8 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-07" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 900 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/Cargo.toml new file mode 100644 index 0000000000..fa58c2aa7f --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-07" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-07" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/src/main.rs new file mode 100644 index 0000000000..bc0b632da2 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-07/rust/src/main.rs @@ -0,0 +1,40 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-07: C program for Project Euler #1 ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 900 /tmp/claw --allowedTools bash,write prompt 'write a C program that computes the sum of all multiples of 3 or 5 below 1000, compile it with gcc, run it, and save the output to euler1.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/euler1.txt").exists() { + let s = fs::read_to_string("/tmp/work/euler1.txt").unwrap_or_default(); + println!("euler1.txt: {s}"); + if s.contains("233168") { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/qemu-x86_64.toml new file mode 100644 index 0000000000..bd68ea1d73 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-08" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 900 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/Cargo.toml new file mode 100644 index 0000000000..8e4893d578 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-08" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-08" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/src/main.rs new file mode 100644 index 0000000000..32dea1889e --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-08/rust/src/main.rs @@ -0,0 +1,40 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-08: word list generation and analysis ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 900 /tmp/claw --allowedTools bash,write prompt 'create a text file with 20 random words, then use bash to sort them alphabetically, count characters per word, and save the full analysis to wordstats.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/wordstats.txt").exists() { + let s = fs::read_to_string("/tmp/work/wordstats.txt").unwrap_or_default(); + println!("wordstats.txt: {} lines", s.lines().count()); + println!("ALL_TESTS_DONE"); + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/qemu-x86_64.toml new file mode 100644 index 0000000000..32802a5915 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-09" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 900 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/Cargo.toml new file mode 100644 index 0000000000..bcb999264d --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-09" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-09" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/src/main.rs new file mode 100644 index 0000000000..fe94e09ba1 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-09/rust/src/main.rs @@ -0,0 +1,40 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-09: CSV generation and average score ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 900 /tmp/claw --allowedTools bash,write prompt 'write a shell script that generates a CSV file with headers name,age,score and 10 rows of sample data, then compute the average score using awk or cut and save the average to avg.txt' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/avg.txt").exists() { + let s = fs::read_to_string("/tmp/work/avg.txt").unwrap_or_default(); + println!("avg.txt: {s}"); + if s.chars().any(|c| c.is_ascii_digit()) { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/qemu-x86_64.toml new file mode 100644 index 0000000000..325348ec94 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-10" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 900 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/Cargo.toml new file mode 100644 index 0000000000..29756fb0d8 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-10" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-10" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/src/main.rs new file mode 100644 index 0000000000..da0bfd05bf --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-10/rust/src/main.rs @@ -0,0 +1,41 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-10: system reconnaissance report ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 900 /tmp/claw --allowedTools bash,write prompt 'create a comprehensive system reconnaissance report: check uname -a, list mount points, show memory info from /proc/meminfo, list all running processes, and compile everything into a sysreport.md file' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/sysreport.md").exists() { + let s = fs::read_to_string("/tmp/work/sysreport.md").unwrap_or_default(); + println!("sysreport.md: {} lines", s.lines().count()); + let lower = s.to_lowercase(); + if lower.contains("uname") || lower.contains("linux") || lower.contains("starry") { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/qemu-x86_64.toml new file mode 100644 index 0000000000..cd17349f62 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-11" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 900 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/Cargo.toml new file mode 100644 index 0000000000..95b655ec31 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-11" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-11" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/src/main.rs new file mode 100644 index 0000000000..2214052659 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-11/rust/src/main.rs @@ -0,0 +1,41 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + #[cfg(unix)] { fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write("/tmp/work/.git/config", "[core]\n\trepositoryformatversion = 0\n\tbare = false\n").ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + println!("=== Robust-11: data pipeline with statistics ==="); + let output = Command::new("sh").arg("-c").arg(format!("cd /tmp/work && {env} timeout 900 /tmp/claw --allowedTools bash,write prompt 'write a multi-step data pipeline: generate 100 random numbers, sort them, compute min/max/average/median, and write a summary report to stats.md' 2>&1")).output().unwrap(); + println!("{}", String::from_utf8_lossy(&output.stdout)); + + if std::path::Path::new("/tmp/work/stats.md").exists() { + let s = fs::read_to_string("/tmp/work/stats.md").unwrap_or_default(); + println!("stats.md: {} lines", s.lines().count()); + let lower = s.to_lowercase(); + if lower.contains("min") || lower.contains("max") || lower.contains("average") || lower.contains("median") { println!("ALL_TESTS_DONE"); } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } + } else { println!("ALL_TESTS_FAILED"); std::process::exit(1); } +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/qemu-x86_64.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/qemu-x86_64.toml new file mode 100644 index 0000000000..6fb03efbf6 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/qemu-x86_64.toml @@ -0,0 +1,15 @@ +args = [ + "-nographic", + "-m", "512M", + "-device", "virtio-blk-pci,drive=disk0", + "-drive", "id=disk0,if=none,format=raw,file=${workspace}/target/rootfs/rootfs-x86_64-alpine.img", + "-device", "virtio-net-pci,netdev=net0", + "-netdev", "user,id=net0" +] +uefi = false +to_bin = false +shell_prefix = "root@starry:" +shell_init_cmd = "/usr/bin/claw-robust-12" +success_regex = ["ALL_TESTS_DONE", "CLAW_SKIP"] +fail_regex = ['(?i)\bpanic(?:ked)?\b', 'ALL_TESTS_FAILED'] +timeout = 1800 diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/Cargo.lock b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/Cargo.lock new file mode 100644 index 0000000000..899e960954 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "claw-robust-12" +version = "0.1.0" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/Cargo.toml b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/Cargo.toml new file mode 100644 index 0000000000..39e0725cb5 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/Cargo.toml @@ -0,0 +1,10 @@ +[workspace] + +[package] +name = "claw-robust-12" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "claw-robust-12" +path = "src/main.rs" diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/build.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/build.rs new file mode 100644 index 0000000000..c55b5017e7 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/build.rs @@ -0,0 +1,40 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; + +fn main() { + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let claw_out = out_dir.join("claw-binary"); + + // Build claw from source via shared build script (caches result). + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_script = manifest_dir.parent().unwrap().parent().unwrap().join("build-claw.sh"); + + let output = Command::new("bash") + .arg(&build_script) + .output() + .expect("failed to run build-claw.sh"); + + if !output.status.success() { + panic!( + "build-claw.sh failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + } + + let stdout = String::from_utf8(output.stdout).expect("invalid utf8 from build-claw.sh"); + // Last non-empty line is the binary path; earlier lines are progress messages. + let claw_bin = stdout.lines().filter(|l| !l.trim().is_empty()).last() + .expect("build-claw.sh produced no output") + .trim(); + assert!(!claw_bin.is_empty(), "build-claw.sh produced empty path"); + + fs::copy(claw_bin, &claw_out).unwrap_or_else(|e| { + panic!("failed to copy claw binary from {claw_bin}: {e}") + }); + + println!("cargo:warning=claw built from source: {claw_bin}"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/src/main.rs b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/src/main.rs new file mode 100644 index 0000000000..0fa4ecce8b --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/claw-code/robust-12/rust/src/main.rs @@ -0,0 +1,81 @@ +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::process::Command; + +const CLAW_PATH: &str = "/tmp/claw"; +fn claw_env() -> Option { + let api_key = std::env::var("CLAW_API_KEY").ok()?; + let api_base = std::env::var("CLAW_API_BASE_URL") + .unwrap_or_else(|_| "https://api.deepseek.com/anthropic".into()); + let api_model = + std::env::var("CLAW_API_MODEL").unwrap_or_else(|_| "deepseek-chat".into()); + Some(format!( + "ANTHROPIC_BASE_URL={api_base} ANTHROPIC_AUTH_TOKEN={api_key} ANTHROPIC_MODEL={api_model}", + )) +} + + + +fn main() { + let claw_bytes: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/claw-binary")); + fs::write(CLAW_PATH, claw_bytes).expect("failed to write claw binary"); + fs::set_permissions(CLAW_PATH, fs::metadata(CLAW_PATH).unwrap().permissions()).ok(); + #[cfg(unix)] + { + fs::set_permissions(CLAW_PATH, std::fs::Permissions::from_mode(0o755)).ok(); + } + + fs::create_dir_all("/tmp/work/.git/refs/heads").ok(); + fs::create_dir_all("/tmp/work/.git/objects").ok(); + fs::write("/tmp/work/.git/HEAD", "ref: refs/heads/master\n").ok(); + fs::write( + "/tmp/work/.git/config", + "[core]\n\trepositoryformatversion = 0\n\tbare = false\n", + ) + .ok(); + + let Some(env) = claw_env() else { + println!("CLAW_SKIP: CLAW_API_KEY not set"); + std::process::exit(0); + }; + + println!("=== Robust-12: multi-agent — spawn 2 sub-agents in parallel ==="); + let (code, _out, _err) = run(&format!( + "cd /tmp/work && {env} timeout 600 /tmp/claw --allowedTools bash,write,Agent prompt 'Use two sub-agents in parallel. \ + Sub-agent 1: create file /tmp/work/a.txt with content \"apple\". \ + Sub-agent 2: create file /tmp/work/b.txt with content \"banana\". \ + After both finish, create /tmp/work/merged.txt combining both contents on separate lines. \ + Say exactly DONE when complete.' 2>&1" + )); + println!("EXIT:{code}"); + + let a_exists = std::path::Path::new("/tmp/work/a.txt").exists(); + let b_exists = std::path::Path::new("/tmp/work/b.txt").exists(); + let merged_exists = std::path::Path::new("/tmp/work/merged.txt").exists(); + println!("a.txt exists: {a_exists}"); + println!("b.txt exists: {b_exists}"); + println!("merged.txt exists: {merged_exists}"); + + if a_exists && b_exists && merged_exists { + let merged = fs::read_to_string("/tmp/work/merged.txt").unwrap_or_default(); + println!("merged.txt content: {merged:?}"); + println!("ALL_TESTS_DONE"); + } else { + println!("ALL_TESTS_FAILED"); + std::process::exit(1); + } +} + +fn run(cmd: &str) -> (i32, String, String) { + let output = Command::new("sh") + .arg("-c") + .arg(cmd) + .output() + .expect("command failed"); + let code = output.status.code().unwrap_or(-1); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + print!("{stdout}"); + eprint!("{stderr}"); + (code, stdout, stderr) +} diff --git a/test-suit/starryos/normal/qemu-smp1/rust-hello/rust/Cargo.lock b/test-suit/starryos/normal/qemu-smp1/rust-hello/rust/Cargo.lock new file mode 100644 index 0000000000..7477ec9e81 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/rust-hello/rust/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "rust-hello" +version = "0.1.0" diff --git a/test-suit/starryos/normal/qemu-smp1/rust-hello/rust/src/prebuild_marker.txt b/test-suit/starryos/normal/qemu-smp1/rust-hello/rust/src/prebuild_marker.txt new file mode 100644 index 0000000000..de702331b9 --- /dev/null +++ b/test-suit/starryos/normal/qemu-smp1/rust-hello/rust/src/prebuild_marker.txt @@ -0,0 +1 @@ +prebuild-ok