Skip to content

Commit 294ba13

Browse files
committed
fix(tar): tighten listing parity and add e2e
1 parent 92cd246 commit 294ba13

4 files changed

Lines changed: 186 additions & 2 deletions

File tree

docs-internal/registry-parity-worklist.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,20 @@ real e2e tests that prove Linux-parity behavior — not smoke tests.
586586
Biome is not applicable for this package test path; it reported the file is
587587
ignored by config in
588588
`2026-07-08T13-17-55-0700-item12-sed-biome-check.log`. Rev: `wvpklkqv`.
589+
- **tar — DONE.** Added package-local VM e2e coverage for the staged `tar`
590+
command and tightened the tar wrapper for Linux-like directory listing and
591+
missing-input error context. The suite proves archive creation/listing,
592+
extraction into `-C` directories, gzip auto-detection by extension,
593+
`--strip-components`, and missing create-input errors through the packaged
594+
WASM command. Proof:
595+
`2026-07-08T13-21-48-0700-item12-tar-toolchain-cmd-build-clean-vendor.log`;
596+
`2026-07-08T13-22-51-0700-item12-tar-package-build-after-wrapper-fix.log`;
597+
`2026-07-08T13-23-02-0700-item12-tar-package-e2e-final.log`;
598+
`2026-07-08T13-23-02-0700-item12-tar-check-types-final.log`;
599+
`2026-07-08T13-23-02-0700-item12-tar-cargo-fmt-check-final.log`.
600+
Biome is not applicable for this package test path; it reported the file is
601+
ignored by config in
602+
`2026-07-08T13-23-12-0700-item12-tar-biome-check.log`. Rev: `rszmulmk`.
589603
- **jq — DONE.** Added package-local VM e2e coverage for the staged `jq`
590604
command and fixed the jaq-backed CLI wrapper to accept Linux-style file
591605
operands instead of only stdin. The suite now proves version output,

software/tar/bin/tar

787 Bytes
Binary file not shown.

software/tar/native/crates/tar/src/lib.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,9 @@ fn append_path<W: Write>(
292292
}
293293
increment_entry_count(entry_count)?;
294294

295-
let meta = fs::symlink_metadata(&disk_path)?;
295+
let meta = fs::symlink_metadata(&disk_path).map_err(|e| {
296+
io::Error::new(e.kind(), format!("metadata {}: {}", disk_path.display(), e))
297+
})?;
296298

297299
if meta.is_dir() {
298300
append_dir(
@@ -548,11 +550,12 @@ fn do_list(archive_file: Option<&str>, gzip: bool, verbose: bool) -> io::Result<
548550
let entry = entry_result?;
549551
let path = entry.path()?;
550552

553+
let entry_type = entry.header().entry_type();
551554
if verbose {
552555
let h = entry.header();
553556
let size = h.size().unwrap_or(0);
554557
let mode = h.mode().unwrap_or(0o644);
555-
let type_ch = match h.entry_type() {
558+
let type_ch = match entry_type {
556559
tar::EntryType::Directory => 'd',
557560
tar::EntryType::Symlink => 'l',
558561
_ => '-',
@@ -565,6 +568,8 @@ fn do_list(archive_file: Option<&str>, gzip: bool, verbose: bool) -> io::Result<
565568
size,
566569
path.display()
567570
)?;
571+
} else if entry_type == tar::EntryType::Directory {
572+
writeln!(out, "{}/", path.display())?;
568573
} else {
569574
writeln!(out, "{}", path.display())?;
570575
}

software/tar/test/tar.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { existsSync } from "node:fs";
2+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3+
import { tmpdir } from "node:os";
4+
import { dirname, join } from "node:path";
5+
import { fileURLToPath } from "node:url";
6+
import {
7+
NodeFileSystem,
8+
createKernel,
9+
createWasmVmRuntime,
10+
describeIf,
11+
} from "@agentos/test-harness";
12+
import type { Kernel } from "@agentos/test-harness";
13+
import { afterEach, expect, it } from "vitest";
14+
15+
const TAR_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url));
16+
const hasTarPackageBinary = existsSync(join(TAR_COMMAND_DIR, "tar"));
17+
18+
let tempRoot: string | undefined;
19+
20+
function hostPath(path: string): string {
21+
if (!tempRoot) throw new Error("fixture root not initialized");
22+
return join(tempRoot, path.replace(/^\/+/, ""));
23+
}
24+
25+
async function writeFixture(path: string, contents: string): Promise<void> {
26+
const target = hostPath(path);
27+
await mkdir(dirname(target), { recursive: true });
28+
await writeFile(target, contents);
29+
}
30+
31+
async function createTestVFS(): Promise<NodeFileSystem> {
32+
tempRoot = await mkdtemp(join(tmpdir(), "agentos-tar-"));
33+
await writeFixture("/project/src/docs/readme.txt", "hello from docs\n");
34+
await writeFixture("/project/src/data/values.csv", "name,score\nAda,7\nLinus,3\n");
35+
await mkdir(hostPath("/project/out"), { recursive: true });
36+
await mkdir(hostPath("/project/strip"), { recursive: true });
37+
await mkdir(hostPath("/project/gzip-out"), { recursive: true });
38+
return new NodeFileSystem({ root: tempRoot });
39+
}
40+
41+
function lines(stdout: string): string[] {
42+
return stdout.split("\n").filter((line) => line.length > 0);
43+
}
44+
45+
const textDecoder = new TextDecoder();
46+
47+
describeIf(hasTarPackageBinary, "tar command", { timeout: 10_000 }, () => {
48+
let kernel: Kernel | undefined;
49+
50+
afterEach(async () => {
51+
await kernel?.dispose();
52+
kernel = undefined;
53+
if (tempRoot) {
54+
await rm(tempRoot, { recursive: true, force: true });
55+
tempRoot = undefined;
56+
}
57+
});
58+
59+
async function mountFixture(): Promise<void> {
60+
const vfs = await createTestVFS();
61+
kernel = createKernel({ filesystem: vfs });
62+
await kernel.mount(createWasmVmRuntime({ commandDirs: [TAR_COMMAND_DIR] }));
63+
}
64+
65+
async function runTar(args: string[]) {
66+
if (!kernel) throw new Error("kernel not mounted");
67+
let stdout = "";
68+
let stderr = "";
69+
const proc = kernel.spawn("tar", args, {
70+
onStdout: (chunk) => {
71+
stdout += Buffer.from(chunk).toString("utf8");
72+
},
73+
onStderr: (chunk) => {
74+
stderr += Buffer.from(chunk).toString("utf8");
75+
},
76+
});
77+
const exitCode = await proc.wait();
78+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
79+
return { stdout, stderr, exitCode };
80+
}
81+
82+
async function createArchive(path = "/project/archive.tar") {
83+
const result = await runTar(["-cf", path, "-C", "/project", "src"]);
84+
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
85+
}
86+
87+
async function readGuestText(path: string): Promise<string> {
88+
if (!kernel) throw new Error("kernel not mounted");
89+
return textDecoder.decode(await kernel.readFile(path));
90+
}
91+
92+
it("creates and lists file-backed archives", async () => {
93+
await mountFixture();
94+
await createArchive();
95+
96+
const result = await runTar(["-tf", "/project/archive.tar"]);
97+
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
98+
expect(lines(result.stdout)).toEqual(
99+
expect.arrayContaining([
100+
"src/",
101+
"src/docs/",
102+
"src/docs/readme.txt",
103+
"src/data/",
104+
"src/data/values.csv",
105+
]),
106+
);
107+
});
108+
109+
it("extracts archives into target directories", async () => {
110+
await mountFixture();
111+
await createArchive();
112+
113+
const result = await runTar(["-xf", "/project/archive.tar", "-C", "/project/out"]);
114+
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
115+
await expect(readGuestText("/project/out/src/docs/readme.txt")).resolves.toBe(
116+
"hello from docs\n",
117+
);
118+
await expect(readGuestText("/project/out/src/data/values.csv")).resolves.toBe(
119+
"name,score\nAda,7\nLinus,3\n",
120+
);
121+
});
122+
123+
it("auto-detects gzip archives by extension", async () => {
124+
await mountFixture();
125+
126+
const create = await runTar(["-czf", "/project/archive.tgz", "-C", "/project", "src"]);
127+
expect(create.exitCode, create.stderr || create.stdout).toBe(0);
128+
129+
const list = await runTar(["-tf", "/project/archive.tgz"]);
130+
expect(list.exitCode, list.stderr || list.stdout).toBe(0);
131+
expect(lines(list.stdout)).toContain("src/docs/readme.txt");
132+
133+
const extract = await runTar(["-xf", "/project/archive.tgz", "-C", "/project/gzip-out"]);
134+
expect(extract.exitCode, extract.stderr || extract.stdout).toBe(0);
135+
await expect(readGuestText("/project/gzip-out/src/docs/readme.txt")).resolves.toBe(
136+
"hello from docs\n",
137+
);
138+
});
139+
140+
it("strips path components on extraction", async () => {
141+
await mountFixture();
142+
await createArchive();
143+
144+
const result = await runTar([
145+
"-xf",
146+
"/project/archive.tar",
147+
"-C",
148+
"/project/strip",
149+
"--strip-components=1",
150+
]);
151+
expect(result.exitCode, result.stderr || result.stdout).toBe(0);
152+
await expect(readGuestText("/project/strip/docs/readme.txt")).resolves.toBe(
153+
"hello from docs\n",
154+
);
155+
await expect(readGuestText("/project/strip/src/docs/readme.txt")).rejects.toThrow();
156+
});
157+
158+
it("fails when a create input is missing", async () => {
159+
await mountFixture();
160+
161+
const result = await runTar(["-cf", "/project/missing.tar", "-C", "/project", "nope"]);
162+
expect(result.exitCode).not.toBe(0);
163+
expect(result.stderr).toContain("nope");
164+
});
165+
});

0 commit comments

Comments
 (0)