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
14 changes: 14 additions & 0 deletions docs-internal/registry-parity-worklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,20 @@ real e2e tests that prove Linux-parity behavior — not smoke tests.
Biome is not applicable for this package test path; it reported the file is
ignored by config in
`2026-07-08T13-35-04-0700-item12-diffutils-biome-check.log`.
- **file — DONE.** Added package-local VM e2e coverage for the staged `file`
command and fixed shebang script classification to run before generic magic
detection, producing Linux-like script descriptions instead of raw
`text/x-shellscript`. The suite proves text, JSON, script, PNG, empty-file,
directory, brief, MIME, stdin, and missing-input behavior through the
packaged WASM command. Proof:
`2026-07-08T13-39-36-0700-item12-file-toolchain-cmd-build.log`;
`2026-07-08T13-40-33-0700-item12-file-package-build-after-shebang-fix.log`;
`2026-07-08T13-41-05-0700-item12-file-package-e2e-final-after-install.log`;
`2026-07-08T13-41-05-0700-item12-file-check-types-final-after-install.log`;
`2026-07-08T13-40-47-0700-item12-file-cargo-fmt-check-final.log`.
Biome is not applicable for this package test path; it reported the file is
ignored by config in
`2026-07-08T13-41-05-0700-item12-file-biome-check-final-after-install.log`.
- **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,
Expand Down
Binary file modified software/file/bin/file
Binary file not shown.
64 changes: 32 additions & 32 deletions software/file/native/crates/file-cmd/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,38 @@ fn identify(data: &[u8]) -> (String, String) {
return ("empty".to_string(), "inode/x-empty".to_string());
}

// Check for shebang scripts
if data.len() >= 2 && data[0] == b'#' && data[1] == b'!' {
let first_line = data
.iter()
.take(128)
.take_while(|&&b| b != b'\n')
.copied()
.collect::<Vec<u8>>();
if let Ok(line) = std::str::from_utf8(&first_line) {
let interp = line.trim_start_matches("#!");
let interp = interp.trim();
// Extract interpreter name
let name = interp.split_whitespace().next().unwrap_or(interp);
let basename = match name.rfind('/') {
Some(pos) => &name[pos + 1..],
None => name,
};
// Handle "env <prog>" pattern
if basename == "env" {
let prog = interp.split_whitespace().nth(1).unwrap_or("script");
return (
format!("{} script, ASCII text executable", prog),
"text/x-script".to_string(),
);
}
return (
format!("{} script, ASCII text executable", basename),
"text/x-script".to_string(),
);
}
}

// Try infer crate for magic byte detection
if let Some(kind) = infer::get(data) {
let desc = match kind.mime_type() {
Expand Down Expand Up @@ -231,38 +263,6 @@ fn identify(data: &[u8]) -> (String, String) {
return (desc.to_string(), kind.mime_type().to_string());
}

// Check for shebang scripts
if data.len() >= 2 && data[0] == b'#' && data[1] == b'!' {
let first_line = data
.iter()
.take(128)
.take_while(|&&b| b != b'\n')
.copied()
.collect::<Vec<u8>>();
if let Ok(line) = std::str::from_utf8(&first_line) {
let interp = line.trim_start_matches("#!");
let interp = interp.trim();
// Extract interpreter name
let name = interp.split_whitespace().next().unwrap_or(interp);
let basename = match name.rfind('/') {
Some(pos) => &name[pos + 1..],
None => name,
};
// Handle "env <prog>" pattern
if basename == "env" {
let prog = interp.split_whitespace().nth(1).unwrap_or("script");
return (
format!("{} script, ASCII text executable", prog),
"text/x-script".to_string(),
);
}
return (
format!("{} script, ASCII text executable", basename),
"text/x-script".to_string(),
);
}
}

// Check for known text patterns
if is_json(data) {
return ("JSON text data".to_string(), "application/json".to_string());
Expand Down
152 changes: 152 additions & 0 deletions software/file/test/file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 FILE_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url));
const hasFilePackageBinary = existsSync(join(FILE_COMMAND_DIR, "file"));

let tempRoot: string | undefined;

async function writeFixture(path: string, contents: string | Buffer): 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-file-"));
await writeFixture("/project/text.txt", "hello from AgentOS\n");
await writeFixture("/project/data.json", '{ "ok": true }\n');
await writeFixture("/project/script.sh", "#!/usr/bin/env bash\necho hello\n");
await writeFixture(
"/project/image.png",
Buffer.from([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
0x0d,
]),
);
await writeFixture("/project/empty", "");
await mkdir(join(tempRoot, "project/dir"), { recursive: true });
return new NodeFileSystem({ root: tempRoot });
}

describeIf(hasFilePackageBinary, "file 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: [FILE_COMMAND_DIR] }));
}

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

it("identifies text, JSON, scripts, images, empty files, and directories", async () => {
await mountFixture();

const result = await runFile([
"/project/text.txt",
"/project/data.json",
"/project/script.sh",
"/project/image.png",
"/project/empty",
"/project/dir",
]);

expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(result.stdout).toContain("/project/text.txt: ASCII text");
expect(result.stdout).toContain("/project/data.json: JSON text data");
expect(result.stdout).toContain(
"/project/script.sh: bash script, ASCII text executable",
);
expect(result.stdout).toContain("/project/image.png: PNG image data");
expect(result.stdout).toContain("/project/empty: empty");
expect(result.stdout).toContain("/project/dir: directory");
});

it("prints brief descriptions with -b", async () => {
await mountFixture();

const result = await runFile(["-b", "/project/data.json"]);

expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(result.stdout.trim()).toBe("JSON text data");
});

it("prints MIME types with -i", async () => {
await mountFixture();

const result = await runFile([
"-i",
"/project/text.txt",
"/project/image.png",
"/project/dir",
]);

expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(result.stdout).toContain("/project/text.txt: text/plain");
expect(result.stdout).toContain("/project/image.png: image/png");
expect(result.stdout).toContain("/project/dir: inode/directory");
});

it("reads stdin when the operand is -", async () => {
await mountFixture();

const result = await runFile(
["-b", "-"],
"#!/usr/bin/env node\nconsole.log('hello')\n",
);

expect(result.exitCode, result.stderr || result.stdout).toBe(0);
expect(result.stdout.trim()).toBe("node script, ASCII text executable");
});

it("returns an error for missing inputs", async () => {
await mountFixture();

const result = await runFile(["/project/missing.txt"]);

expect(result.exitCode).toBe(1);
expect(result.stderr).toContain("/project/missing.txt");
});
});