Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs-internal/registry-parity-worklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,18 @@ real e2e tests that prove Linux-parity behavior — not smoke tests.
### 12. No tests at all — 9 software + 5 agents
- **Broken:** zero e2e coverage: `gawk, sed, tar, gzip, jq, yq, diffutils,
file, vim`; agents `claude, codex, opencode, pi, pi-cli`.
- **Status:**
- **jq — DONE.** Added package-local VM e2e coverage for the staged `jq`
command and fixed the jaq-backed CLI wrapper to accept Linux-style file
operands instead of only stdin. The suite now proves version output,
file-backed array filtering, aggregate JSON construction, slurped NDJSON,
and invalid JSON parse errors through the packaged WASM command. Proof:
`2026-07-08T13-10-55-0700-item12-jq-toolchain-cmd-build-isolated-target.log`;
`2026-07-08T13-12-18-0700-item12-jq-package-build-after-wrapper-fix.log`;
`2026-07-08T13-12-51-0700-item12-jq-package-e2e-final.log`;
`2026-07-08T13-12-51-0700-item12-jq-check-types-final.log`;
`2026-07-08T13-12-51-0700-item12-jq-cargo-fmt-check-final.log`.
Rev: `slnmvuqz`.
- **Objective:** write real e2e tests proving each behaves like its Linux
counterpart (jq processes real JSON, sed edits streams, tar round-trips archives,
gzip round-trips, etc.); agents exercise the real ACP
Expand Down
Binary file modified software/jq/bin/jq
Binary file not shown.
102 changes: 79 additions & 23 deletions software/jq/native/crates/jq/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! Wraps jaq-core/jaq-std/jaq-json to provide a standard jq CLI interface.

use std::ffi::OsString;
use std::fs::File as FsFile;
use std::io::{self, Read, Write};

use jaq_core::load::{Arena, File, Loader};
Expand Down Expand Up @@ -40,6 +41,7 @@ struct JqOptions {
join_output: bool,
args: Vec<(String, String)>,
jsonargs: Vec<(String, Val)>,
input_paths: Vec<String>,
}

fn parse_args(args: &[String]) -> Result<JqOptions, String> {
Expand All @@ -54,6 +56,7 @@ fn parse_args(args: &[String]) -> Result<JqOptions, String> {
join_output: false,
args: Vec::new(),
jsonargs: Vec::new(),
input_paths: Vec::new(),
};

let mut filter_set = false;
Expand All @@ -63,6 +66,17 @@ fn parse_args(args: &[String]) -> Result<JqOptions, String> {
let arg = &args[i];

if arg == "--" {
let remaining = &args[i + 1..];
if !filter_set {
let Some(filter) = remaining.first() else {
return Err("no filter provided".to_string());
};
opts.filter = filter.clone();
filter_set = true;
opts.input_paths.extend(remaining.iter().skip(1).cloned());
} else {
opts.input_paths.extend(remaining.iter().cloned());
}
break;
}

Expand Down Expand Up @@ -118,7 +132,7 @@ fn parse_args(args: &[String]) -> Result<JqOptions, String> {
opts.filter = arg.clone();
filter_set = true;
} else {
return Err(format!("unexpected argument: {}", arg));
opts.input_paths.push(arg.clone());
}

i += 1;
Expand All @@ -131,30 +145,60 @@ fn parse_args(args: &[String]) -> Result<JqOptions, String> {
Ok(opts)
}

fn read_sources(opts: &JqOptions) -> Result<Vec<String>, String> {
if opts.input_paths.is_empty() {
let mut stdin_data = String::new();
io::stdin()
.take((MAX_INPUT_BYTES + 1) as u64)
.read_to_string(&mut stdin_data)
.map_err(|e| format!("failed to read stdin: {}", e))?;
if stdin_data.len() > MAX_INPUT_BYTES {
return Err("stdin exceeds size limit".to_string());
}
return Ok(vec![stdin_data]);
}

let mut total = 0usize;
let mut sources = Vec::new();
for path in &opts.input_paths {
let mut data = String::new();
if path == "-" {
io::stdin()
.take((MAX_INPUT_BYTES.saturating_sub(total) + 1) as u64)
.read_to_string(&mut data)
.map_err(|e| format!("failed to read stdin: {}", e))?;
} else {
FsFile::open(path)
.map_err(|e| format!("failed to open {}: {}", path, e))?
.take((MAX_INPUT_BYTES.saturating_sub(total) + 1) as u64)
.read_to_string(&mut data)
.map_err(|e| format!("failed to read {}: {}", path, e))?;
}
total = total
.checked_add(data.len())
.ok_or_else(|| "input exceeds size limit".to_string())?;
if total > MAX_INPUT_BYTES {
return Err("input exceeds size limit".to_string());
}
sources.push(data);
}
Ok(sources)
}

fn read_inputs(opts: &JqOptions) -> Result<Vec<Val>, String> {
if opts.null_input {
return Ok(vec![Val::from(serde_json::Value::Null)]);
}

let mut stdin_data = String::new();
io::stdin()
.take((MAX_INPUT_BYTES + 1) as u64)
.read_to_string(&mut stdin_data)
.map_err(|e| format!("failed to read stdin: {}", e))?;
if stdin_data.len() > MAX_INPUT_BYTES {
return Err("stdin exceeds size limit".to_string());
}
let sources = read_sources(opts)?;

if opts.raw_input {
let raw_data = sources.concat();
if opts.slurp {
let mut arr = Vec::new();
for line in stdin_data.lines() {
push_input_value(&mut arr, serde_json::Value::String(line.to_string()))?;
}
Ok(vec![Val::from(serde_json::Value::Array(arr))])
Ok(vec![Val::from(serde_json::Value::String(raw_data))])
} else {
let mut lines = Vec::new();
for line in stdin_data.lines() {
for line in raw_data.lines() {
push_input_value(
&mut lines,
Val::from(serde_json::Value::String(line.to_string())),
Expand All @@ -163,16 +207,23 @@ fn read_inputs(opts: &JqOptions) -> Result<Vec<Val>, String> {
Ok(lines)
}
} else {
let trimmed = stdin_data.trim();
if trimmed.is_empty() {
return Ok(vec![Val::from(serde_json::Value::Null)]);
let mut values = Vec::new();
for source in &sources {
let trimmed = source.trim();
if trimmed.is_empty() {
continue;
}

let decoder =
serde_json::Deserializer::from_str(trimmed).into_iter::<serde_json::Value>();
for result in decoder {
let value = result.map_err(|e| format!("parse error: {}", e))?;
push_input_value(&mut values, value)?;
}
}

let mut values = Vec::new();
let decoder = serde_json::Deserializer::from_str(trimmed).into_iter::<serde_json::Value>();
for result in decoder {
let value = result.map_err(|e| format!("invalid JSON input: {}", e))?;
push_input_value(&mut values, value)?;
if values.is_empty() && opts.input_paths.is_empty() {
values.push(serde_json::Value::Null);
}

if opts.slurp {
Expand Down Expand Up @@ -217,6 +268,11 @@ fn format_output(val: &Val, opts: &JqOptions) -> Result<String, String> {
}

fn run_jq(args: &[String]) -> Result<i32, String> {
if matches!(args, [arg] if arg == "--version" || arg == "-V") {
println!("jq-jaq-{}", env!("CARGO_PKG_VERSION"));
return Ok(0);
}

let opts = parse_args(args)?;
let inputs = read_inputs(&opts)?;

Expand Down
153 changes: 153 additions & 0 deletions software/jq/test/jq.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { existsSync } from "node:fs";
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import {
NodeFileSystem,
createKernel,
createWasmVmRuntime,
describeIf,
} from "@agentos/test-harness";
import type { Kernel } from "@agentos/test-harness";
import { afterEach, expect, it } from "vitest";

const JQ_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url));
const hasJqPackageBinary = existsSync(join(JQ_COMMAND_DIR, "jq"));

let tempRoot: string | undefined;

async function writeFixture(path: string, contents: string): Promise<void> {
if (!tempRoot) throw new Error("fixture root not initialized");
const hostPath = join(tempRoot, path.replace(/^\/+/, ""));
await mkdir(dirname(hostPath), { recursive: true });
await writeFile(hostPath, contents);
}

async function createTestVFS(): Promise<NodeFileSystem> {
tempRoot = await mkdtemp(join(tmpdir(), "agentos-jq-"));
await writeFixture(
"/project/users.json",
`${JSON.stringify(
{
users: [
{ name: "Ada", active: true, team: "runtime", score: 7 },
{ name: "Grace", active: false, team: "docs", score: 5 },
{ name: "Linus", active: true, team: "runtime", score: 3 },
],
},
null,
2,
)}\n`,
);
await writeFixture(
"/project/events.ndjson",
[
JSON.stringify({ type: "build", value: 4 }),
JSON.stringify({ type: "test", value: 6 }),
JSON.stringify({ type: "deploy", value: 2 }),
].join("\n") + "\n",
);
await writeFixture("/project/broken.json", '{"users": [');
return new NodeFileSystem({ root: tempRoot });
}

function lines(stdout: string): string[] {
return stdout.split("\n").filter((line) => line.length > 0);
}

describeIf(hasJqPackageBinary, "jq command", { timeout: 10_000 }, () => {
let kernel: Kernel | undefined;

afterEach(async () => {
await kernel?.dispose();
kernel = undefined;
if (tempRoot) {
await rm(tempRoot, { recursive: true, force: true });
tempRoot = undefined;
}
});

async function mountFixture(): Promise<void> {
const vfs = await createTestVFS();
kernel = createKernel({ filesystem: vfs });
await kernel.mount(createWasmVmRuntime({ commandDirs: [JQ_COMMAND_DIR] }));
}

async function runJq(args: string[]) {
if (!kernel) throw new Error("kernel not mounted");
let stdout = "";
let stderr = "";
const proc = kernel.spawn("jq", args, {
onStdout: (chunk) => {
stdout += Buffer.from(chunk).toString("utf8");
},
onStderr: (chunk) => {
stderr += Buffer.from(chunk).toString("utf8");
},
});
const exitCode = await proc.wait();
await new Promise<void>((resolve) => setTimeout(resolve, 0));
return { stdout, stderr, exitCode };
}

it("reports a jq-compatible version", async () => {
await mountFixture();

const result = await runJq(["--version"]);
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(result.stdout).toMatch(/^jq-/);
});

it("filters arrays and emits raw strings", async () => {
await mountFixture();

const result = await runJq([
"-r",
'.users[] | select(.active) | "\\(.name):\\(.score)"',
"/project/users.json",
]);
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(lines(result.stdout)).toEqual(["Ada:7", "Linus:3"]);
});

it("builds aggregate JSON objects", async () => {
await mountFixture();

const result = await runJq([
"-c",
'{activeNames: [.users[] | select(.active) | .name], runtimeTotal: ([.users[] | select(.team == "runtime") | .score] | add)}',
"/project/users.json",
]);
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(JSON.parse(result.stdout.trim())).toEqual({
activeNames: ["Ada", "Linus"],
runtimeTotal: 10,
});
});

it("slurps newline-delimited JSON records", async () => {
await mountFixture();

const result = await runJq([
"-s",
"-c",
"{count: length, total: (map(.value) | add), types: map(.type)}",
"/project/events.ndjson",
]);
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(JSON.parse(result.stdout.trim())).toEqual({
count: 3,
total: 12,
types: ["build", "test", "deploy"],
});
});

it("fails with a parse error for invalid JSON", async () => {
await mountFixture();

const result = await runJq([".", "/project/broken.json"]);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain("parse error");
});
});