Skip to content

Commit 546c727

Browse files
authored
Merge pull request #206 from skulidropek/issue-198-session-backup-tests
test(scripts): cover session backup tmp filtering
2 parents 24a7847 + 8a21a58 commit 546c727

File tree

3 files changed

+119
-2
lines changed

3 files changed

+119
-2
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// CHANGE: add regression coverage for session backup tmp filtering
2+
// WHY: session snapshots must ignore transient tmp directories while preserving persistent files
3+
// REF: issue-198
4+
// PURITY: SHELL (tests filesystem traversal against committed backup script)
5+
6+
import { describe, expect, it } from "@effect/vitest"
7+
import { Effect } from "effect"
8+
import fs from "node:fs"
9+
import path from "node:path"
10+
11+
import sessionBackupGist from "../../../../scripts/session-backup-gist.js"
12+
13+
const { collectSessionFiles, shouldIgnoreSessionPath } = sessionBackupGist
14+
const tmpDirPrefix = path.join(process.cwd(), ".tmp-session-backup-gist-")
15+
16+
const withTempDir = Effect.acquireRelease(
17+
Effect.sync(() => fs.mkdtempSync(tmpDirPrefix)),
18+
(tmpDir) =>
19+
Effect.sync(() => {
20+
fs.rmSync(tmpDir, { recursive: true, force: true })
21+
})
22+
)
23+
24+
describe("session-backup-gist tmp filtering", () => {
25+
it.effect("ignores tmp directories while keeping persistent session files", () =>
26+
Effect.scoped(
27+
Effect.gen(function*(_) {
28+
const tmpDir = yield* _(withTempDir)
29+
const codexDir = path.join(tmpDir, ".codex")
30+
const claudeDir = path.join(tmpDir, ".claude")
31+
32+
yield* _(
33+
Effect.sync(() => {
34+
fs.mkdirSync(path.join(codexDir, "tmp", "run"), { recursive: true })
35+
fs.mkdirSync(path.join(codexDir, "memory"), { recursive: true })
36+
fs.mkdirSync(path.join(claudeDir, "tmp"), { recursive: true })
37+
fs.mkdirSync(path.join(claudeDir, "profiles"), { recursive: true })
38+
39+
fs.writeFileSync(path.join(codexDir, "tmp", "run", ".lock"), "lock")
40+
fs.writeFileSync(path.join(codexDir, "history.jsonl"), "{\"event\":1}\n")
41+
fs.writeFileSync(path.join(codexDir, "memory", "notes.md"), "# notes\n")
42+
fs.writeFileSync(path.join(claudeDir, "tmp", "session.lock"), "lock")
43+
fs.writeFileSync(path.join(claudeDir, "profiles", "default.json"), "{}\n")
44+
})
45+
)
46+
47+
const files = [
48+
...collectSessionFiles(codexDir, ".codex", false),
49+
...collectSessionFiles(claudeDir, ".claude", false)
50+
]
51+
const logicalNames = files
52+
.map((file) => file.logicalName)
53+
.toSorted((left, right) => left.localeCompare(right))
54+
55+
yield* _(
56+
Effect.sync(() => {
57+
expect(logicalNames).toContain(".codex/history.jsonl")
58+
expect(logicalNames).toContain(".codex/memory/notes.md")
59+
expect(logicalNames).toContain(".claude/profiles/default.json")
60+
expect(logicalNames.some((name) => name.split("/").includes("tmp"))).toBe(false)
61+
})
62+
)
63+
})
64+
))
65+
66+
it.effect("marks tmp paths for exclusion", () =>
67+
Effect.sync(() => {
68+
expect(shouldIgnoreSessionPath("tmp")).toBe(true)
69+
expect(shouldIgnoreSessionPath("tmp/run/.lock")).toBe(true)
70+
expect(shouldIgnoreSessionPath("memory/tmp/run/.lock")).toBe(true)
71+
expect(shouldIgnoreSessionPath("history.jsonl")).toBe(false)
72+
expect(shouldIgnoreSessionPath("memory/notes.md")).toBe(false)
73+
}))
74+
})

scripts/session-backup-gist.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export type SessionBackupFile = {
2+
readonly logicalName: string
3+
readonly sourcePath: string
4+
readonly size: number
5+
}
6+
7+
export declare const collectSessionFiles: (
8+
dirPath: string,
9+
baseName: string,
10+
verbose: boolean
11+
) => ReadonlyArray<SessionBackupFile>
12+
13+
export declare const shouldIgnoreSessionPath: (
14+
relativePath: string
15+
) => boolean
16+
17+
declare const sessionBackupGist: {
18+
readonly collectSessionFiles: typeof collectSessionFiles
19+
readonly shouldIgnoreSessionPath: typeof shouldIgnoreSessionPath
20+
}
21+
22+
export default sessionBackupGist

scripts/session-backup-gist.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ const {
4343
const SESSION_DIR_NAMES = [".codex", ".claude", ".qwen", ".gemini"];
4444
const SESSION_WALK_IGNORE_DIR_NAMES = new Set([".git", "node_modules", "tmp"]);
4545

46+
const toLogicalRelativePath = (relativePath) =>
47+
relativePath.split(path.sep).join(path.posix.sep);
48+
49+
const shouldIgnoreSessionPath = (relativePath) => {
50+
const logicalPath = toLogicalRelativePath(relativePath);
51+
return logicalPath === "tmp" || logicalPath.startsWith("tmp/") || logicalPath.includes("/tmp/");
52+
};
53+
4654
const isPathWithinParent = (targetPath, parentPath) => {
4755
const relative = path.relative(parentPath, targetPath);
4856
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
@@ -365,6 +373,12 @@ const collectSessionFiles = (dirPath, baseName, verbose) => {
365373
for (const entry of entries) {
366374
const fullPath = path.join(currentPath, entry.name);
367375
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
376+
const logicalRelPath = toLogicalRelativePath(relPath);
377+
378+
if (shouldIgnoreSessionPath(logicalRelPath)) {
379+
log(verbose, `Skipping tmp path: ${path.posix.join(baseName, logicalRelPath)}`);
380+
continue;
381+
}
368382

369383
if (entry.isDirectory()) {
370384
if (SESSION_WALK_IGNORE_DIR_NAMES.has(entry.name)) {
@@ -374,7 +388,7 @@ const collectSessionFiles = (dirPath, baseName, verbose) => {
374388
} else if (entry.isFile()) {
375389
try {
376390
const stats = fs.statSync(fullPath);
377-
const logicalName = path.posix.join(baseName, relPath.split(path.sep).join(path.posix.sep));
391+
const logicalName = path.posix.join(baseName, logicalRelPath);
378392
files.push({
379393
logicalName,
380394
sourcePath: fullPath,
@@ -662,4 +676,11 @@ const main = () => {
662676
}
663677
};
664678

665-
main();
679+
if (require.main === module) {
680+
main();
681+
}
682+
683+
module.exports = {
684+
collectSessionFiles,
685+
shouldIgnoreSessionPath,
686+
};

0 commit comments

Comments
 (0)