Skip to content

Commit 0534b67

Browse files
committed
fix(scripts): ignore transient codex tmp session files
1 parent 5182ee0 commit 0534b67

3 files changed

Lines changed: 120 additions & 2 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// CHANGE: add regression coverage for transient session backup paths
2+
// WHY: `.codex/tmp` contains ephemeral runtime files that should not break snapshot creation
3+
// REF: transient-session-backup-tmp
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 os from "node:os"
10+
import path from "node:path"
11+
12+
import sessionBackupGist from "../../../../scripts/session-backup-gist.js"
13+
14+
const { collectSessionFiles, shouldIgnoreSessionPath } = sessionBackupGist
15+
16+
const withTempDir = Effect.acquireRelease(
17+
Effect.sync(() => fs.mkdtempSync(path.join(os.tmpdir(), "session-backup-gist-"))),
18+
(tmpDir) =>
19+
Effect.sync(() => {
20+
fs.rmSync(tmpDir, { recursive: true, force: true })
21+
})
22+
)
23+
24+
describe("session-backup-gist transient path filtering", () => {
25+
it.effect("ignores .codex/tmp 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+
38+
fs.writeFileSync(path.join(codexDir, "tmp", "run", ".lock"), "lock")
39+
fs.writeFileSync(path.join(codexDir, "history.jsonl"), "{\"event\":1}\n")
40+
fs.writeFileSync(path.join(codexDir, "memory", "notes.md"), "# notes\n")
41+
fs.writeFileSync(path.join(claudeDir, "tmp", "should-stay.txt"), "keep")
42+
})
43+
)
44+
45+
const codexFiles = collectSessionFiles(codexDir, ".codex", false)
46+
const claudeFiles = collectSessionFiles(claudeDir, ".claude", false)
47+
const logicalNames = [...codexFiles, ...claudeFiles]
48+
.map((file) => file.logicalName)
49+
.toSorted((left, right) => left.localeCompare(right))
50+
51+
yield* _(
52+
Effect.sync(() => {
53+
expect(logicalNames).toContain(".codex/history.jsonl")
54+
expect(logicalNames).toContain(".codex/memory/notes.md")
55+
expect(logicalNames).toContain(".claude/tmp/should-stay.txt")
56+
expect(logicalNames.some((name) => name.startsWith(".codex/tmp/"))).toBe(false)
57+
})
58+
)
59+
})
60+
))
61+
62+
it.effect("marks only targeted transient .codex tmp paths for exclusion", () =>
63+
Effect.sync(() => {
64+
expect(shouldIgnoreSessionPath(".codex", "tmp")).toBe(true)
65+
expect(shouldIgnoreSessionPath(".codex", "tmp/run/.lock")).toBe(true)
66+
expect(shouldIgnoreSessionPath(".codex", "history.jsonl")).toBe(false)
67+
expect(shouldIgnoreSessionPath(".claude", "tmp/run/.lock")).toBe(false)
68+
}))
69+
})

scripts/session-backup-gist.d.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
baseName: string,
15+
relativePath: string
16+
) => boolean
17+
18+
declare const sessionBackupGist: {
19+
readonly collectSessionFiles: typeof collectSessionFiles
20+
readonly shouldIgnoreSessionPath: typeof shouldIgnoreSessionPath
21+
}
22+
23+
export default sessionBackupGist

scripts/session-backup-gist.js

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ const {
4141
} = require("./session-backup-repo.js");
4242

4343
const SESSION_DIR_NAMES = [".codex", ".claude", ".qwen", ".gemini"];
44+
const TRANSIENT_SESSION_PREFIXES = {
45+
".codex": ["tmp"],
46+
};
4447

4548
const isPathWithinParent = (targetPath, parentPath) => {
4649
const relative = path.relative(parentPath, targetPath);
@@ -71,6 +74,16 @@ const resolveAllowedSessionDir = (candidatePath, verbose) => {
7174
return null;
7275
};
7376

77+
const toLogicalRelativePath = (relativePath) =>
78+
relativePath.split(path.sep).join(path.posix.sep);
79+
80+
const shouldIgnoreSessionPath = (baseName, relativePath) => {
81+
const prefixes = TRANSIENT_SESSION_PREFIXES[baseName] ?? [];
82+
const logicalPath = toLogicalRelativePath(relativePath);
83+
84+
return prefixes.some((prefix) => logicalPath === prefix || logicalPath.startsWith(`${prefix}/`));
85+
};
86+
7487
const parseArgs = () => {
7588
const args = process.argv.slice(2);
7689
const result = {
@@ -364,6 +377,12 @@ const collectSessionFiles = (dirPath, baseName, verbose) => {
364377
for (const entry of entries) {
365378
const fullPath = path.join(currentPath, entry.name);
366379
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
380+
const logicalRelPath = toLogicalRelativePath(relPath);
381+
382+
if (shouldIgnoreSessionPath(baseName, logicalRelPath)) {
383+
log(verbose, `Skipping transient path: ${path.posix.join(baseName, logicalRelPath)}`);
384+
continue;
385+
}
367386

368387
if (entry.isDirectory()) {
369388
if (entry.name === "node_modules" || entry.name === ".git") {
@@ -373,7 +392,7 @@ const collectSessionFiles = (dirPath, baseName, verbose) => {
373392
} else if (entry.isFile()) {
374393
try {
375394
const stats = fs.statSync(fullPath);
376-
const logicalName = path.posix.join(baseName, relPath.split(path.sep).join(path.posix.sep));
395+
const logicalName = path.posix.join(baseName, logicalRelPath);
377396
files.push({
378397
logicalName,
379398
sourcePath: fullPath,
@@ -661,4 +680,11 @@ const main = () => {
661680
}
662681
};
663682

664-
main();
683+
if (require.main === module) {
684+
main();
685+
}
686+
687+
module.exports = {
688+
collectSessionFiles,
689+
shouldIgnoreSessionPath,
690+
};

0 commit comments

Comments
 (0)