Skip to content

Commit 8f5b0af

Browse files
committed
feat: [US-020] - Add missing conformance test coverage from adversarial review
1 parent cd99812 commit 8f5b0af

3 files changed

Lines changed: 145 additions & 0 deletions

File tree

packages/core/src/test/block-store-conformance.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,20 @@ export function defineBlockStoreTests(
162162
expectErrorCode(err, "ENOENT");
163163
}
164164
});
165+
166+
test("readRange with offset exactly at block size returns empty Uint8Array", async () => {
167+
const data = makeData(50);
168+
await store.write("key", data);
169+
const result = await store.readRange("key", 50, 10);
170+
expect(result.length).toBe(0);
171+
});
172+
173+
test("readRange with offset=0, length=0 returns empty Uint8Array", async () => {
174+
const data = makeData(50);
175+
await store.write("key", data);
176+
const result = await store.readRange("key", 0, 0);
177+
expect(result.length).toBe(0);
178+
});
165179
});
166180

167181
// ---------------------------------------------------------------

packages/core/src/test/metadata-store-conformance.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,35 @@ export function defineMetadataStoreTests(
160160
const meta = await store.getInode(99999);
161161
expect(meta).toBeNull();
162162
});
163+
164+
test("deleteInode cleans up associated chunk mappings and symlink targets", async () => {
165+
// Create a file inode with chunk mappings.
166+
const fileIno = await store.createInode(fileAttrs());
167+
await store.setChunkKey(fileIno, 0, "chunk-0");
168+
await store.setChunkKey(fileIno, 1, "chunk-1");
169+
170+
// Create a symlink inode.
171+
const symlinkIno = await store.createInode(symlinkAttrs("/some/target"));
172+
173+
// Delete the file inode.
174+
await store.deleteInode(fileIno);
175+
expect(await store.getInode(fileIno)).toBeNull();
176+
// Chunk mappings should also be cleaned up.
177+
expect(await store.getChunkKey(fileIno, 0)).toBeNull();
178+
expect(await store.getChunkKey(fileIno, 1)).toBeNull();
179+
180+
// Delete the symlink inode.
181+
await store.deleteInode(symlinkIno);
182+
expect(await store.getInode(symlinkIno)).toBeNull();
183+
// readSymlink should fail or return null.
184+
try {
185+
const target = await store.readSymlink(symlinkIno);
186+
// If it doesn't throw, it should return null or undefined.
187+
expect(target == null).toBe(true);
188+
} catch {
189+
// Expected: inode no longer exists.
190+
}
191+
});
163192
});
164193

165194
// ---------------------------------------------------------------
@@ -409,6 +438,22 @@ export function defineMetadataStoreTests(
409438
expectErrorCode(err, "ENOENT");
410439
}
411440
});
441+
442+
test("resolvePath with relative symlink targets", async () => {
443+
// Create /dir/ containing real.txt and a relative symlink link.txt -> real.txt.
444+
const dirIno = await store.createInode(dirAttrs());
445+
await store.createDentry(1, "dir", dirIno, "directory");
446+
447+
const fileIno = await store.createInode(fileAttrs());
448+
await store.createDentry(dirIno, "real.txt", fileIno, "file");
449+
450+
// Relative symlink: target is "real.txt" (no leading /).
451+
const linkIno = await store.createInode(symlinkAttrs("real.txt"));
452+
await store.createDentry(dirIno, "link.txt", linkIno, "symlink");
453+
454+
const resolved = await store.resolvePath("/dir/link.txt");
455+
expect(resolved).toBe(fileIno);
456+
});
412457
});
413458

414459
// ---------------------------------------------------------------
@@ -549,6 +594,17 @@ export function defineMetadataStoreTests(
549594
const target = await store.readSymlink(ino);
550595
expect(target).toBe("/some/target");
551596
});
597+
598+
test("readSymlink on non-symlink inode throws error", async () => {
599+
const fileIno = await store.createInode(fileAttrs());
600+
try {
601+
await store.readSymlink(fileIno);
602+
expect.fail("should have thrown");
603+
} catch (err) {
604+
// Any error is acceptable: EINVAL, ENOENT, or a generic Error.
605+
expect(err).toBeInstanceOf(Error);
606+
}
607+
});
552608
});
553609

554610
// ---------------------------------------------------------------

packages/core/src/test/vfs-conformance.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,81 @@ export function defineVfsConformanceTests(config: VfsConformanceConfig): void {
999999
const text = await fs.readTextFile(path);
10001000
expect(text).toBe("deep");
10011001
});
1002+
1003+
test.skipIf(!capabilities.pwrite)(
1004+
"pwrite on nonexistent file throws ENOENT",
1005+
async () => {
1006+
const err = await fs
1007+
.pwrite("/no-such-file.txt", 0, new TextEncoder().encode("x"))
1008+
.catch((e) => e);
1009+
expectErrorCode(err, "ENOENT");
1010+
},
1011+
);
1012+
1013+
test.skipIf(!capabilities.truncate)(
1014+
"truncate on nonexistent file throws ENOENT",
1015+
async () => {
1016+
const err = await fs.truncate("/no-such-file.txt", 0).catch((e) => e);
1017+
expectErrorCode(err, "ENOENT");
1018+
},
1019+
);
1020+
1021+
test("stat on root directory '/' returns isDirectory: true", async () => {
1022+
const s = await fs.stat("/");
1023+
expect(s.isDirectory).toBe(true);
1024+
});
1025+
});
1026+
1027+
// ---------------------------------------------------------------
1028+
// Relative symlink tests (gated)
1029+
// ---------------------------------------------------------------
1030+
1031+
describe.skipIf(!capabilities.symlinks)("relative symlinks", () => {
1032+
test("relative symlink resolution", async () => {
1033+
// Create /dir/real.txt, then /dir/link.txt -> real.txt (relative)
1034+
await fs.writeFile("/dir/real.txt", "real content");
1035+
await fs.symlink("real.txt", "/dir/link.txt");
1036+
const text = await fs.readTextFile("/dir/link.txt");
1037+
expect(text).toBe("real content");
1038+
});
1039+
1040+
test("symlink-to-directory traversal", async () => {
1041+
// Create /real-dir/file.txt, then /a -> /real-dir
1042+
// Reading /a/file.txt should resolve to /real-dir/file.txt
1043+
await fs.writeFile("/real-dir/file.txt", "traversed");
1044+
await fs.symlink("/real-dir", "/a");
1045+
const text = await fs.readTextFile("/a/file.txt");
1046+
expect(text).toBe("traversed");
1047+
});
1048+
});
1049+
1050+
// ---------------------------------------------------------------
1051+
// Concurrent rename + readFile (gated on pwrite for concurrency)
1052+
// ---------------------------------------------------------------
1053+
1054+
describe.skipIf(!capabilities.pwrite)("concurrent rename + readFile", () => {
1055+
test("concurrent rename + readFile does not crash or corrupt", async () => {
1056+
await fs.writeFile("/conc-rename.txt", "data before rename");
1057+
// Run rename and readFile concurrently. Neither should crash.
1058+
// readFile may see the file at old or new path depending on ordering.
1059+
const results = await Promise.allSettled([
1060+
fs.rename("/conc-rename.txt", "/conc-renamed.txt"),
1061+
fs.readFile("/conc-rename.txt"),
1062+
]);
1063+
// Rename should succeed.
1064+
expect(results[0]!.status).toBe("fulfilled");
1065+
// readFile may succeed (read before rename) or fail with ENOENT (read after rename).
1066+
if (results[1]!.status === "fulfilled") {
1067+
const data = (results[1] as PromiseFulfilledResult<Uint8Array>).value;
1068+
expect(new TextDecoder().decode(data)).toBe("data before rename");
1069+
} else {
1070+
const err = (results[1] as PromiseRejectedResult).reason;
1071+
expectErrorCode(err, "ENOENT");
1072+
}
1073+
// The file should exist at the new path.
1074+
const text = await fs.readTextFile("/conc-renamed.txt");
1075+
expect(text).toBe("data before rename");
1076+
});
10021077
});
10031078
});
10041079
}

0 commit comments

Comments
 (0)