Files
Codeman/test/generated-artifact-attachments.test.ts
Codeman maintainer 13c877f938 fix(review): harden Codex generated-artifact attachment pipeline (PR #150)
- Pass the attachment request `source` through the server deps lambda and make
  it a required param on SessionListenerDeps.registerAttachment + the wiring
  event type (the 2-arg lambda silently dropped `source`, force-confining every
  codex-generated artifact — the feature never worked outside the workspace);
  new test/session-listener-wiring.test.ts asserts the pass-through
- Gate the Codex `Saved to: file://` scanner on mode === 'codex' via a
  codexArtifacts option threaded from the session call site; magic links stay
  mode-agnostic; tests assert claude/shell sessions never emit codex-generated
  requests
- Decide the generated-artifact trust policy on the realpath-RESOLVED path
  (unresolvable → force-confined) and anchor the ~/.codex marker dirs to
  os.homedir() prefixes with startsWith instead of substring matching; symlink
  escape + unanchored-marker regression tests added
- Run the Codex scanner on stripAnsi'd data so trailing SGR sequences don't
  ride into the captured URL; styled 'Saved to:' test added
- Extend generateFirstPageThumbnail with jpg/jpeg/gif/webp passthrough and
  per-extension content types (mirrors the png passthrough) so the PR's new
  image formats render real thumbnails instead of 204 letter-tiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:50:21 +02:00

79 lines
3.2 KiB
TypeScript

import { afterEach, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import {
isAllowedGeneratedArtifactPath,
registerGeneratedArtifactAttachment,
} from '../src/generated-artifact-attachments.js';
import { attachmentRegistry } from '../src/attachment-registry.js';
describe('generated artifact attachments', () => {
it('allows workspace artifacts and home-anchored Codex generated image directories', () => {
const home = homedir();
expect(isAllowedGeneratedArtifactPath('/repo/out/mockup.png', '/repo')).toBe(true);
expect(
isAllowedGeneratedArtifactPath(join(home, '.codex-personal', 'generated_images', 'mockup.png'), '/repo')
).toBe(true);
expect(isAllowedGeneratedArtifactPath(join(home, '.codex', 'generated_artifacts', 'report.pdf'), '/repo')).toBe(
true
);
expect(isAllowedGeneratedArtifactPath('/etc/secret.png', '/repo')).toBe(false);
expect(
isAllowedGeneratedArtifactPath(
join(home, '.codex-personal', 'generated_images', '..', '..', '.ssh', 'id_rsa.png'),
'/repo'
)
).toBe(false);
});
it('rejects .codex marker directories that are not anchored at the user home', () => {
expect(isAllowedGeneratedArtifactPath('/var/tmp/staging/.codex/generated_images/leak.png', '/repo')).toBe(false);
expect(isAllowedGeneratedArtifactPath('/var/tmp/.codex-personal/generated_artifacts/leak.md', '/repo')).toBe(false);
});
describe('symlink resolution', () => {
let workspaceDir: string | undefined;
let outsideDir: string | undefined;
const sessionId = 'generated-artifact-symlink-test';
afterEach(async () => {
attachmentRegistry.clearSession(sessionId);
for (const dir of [workspaceDir, outsideDir]) {
if (dir) await fs.rm(dir, { recursive: true, force: true });
}
workspaceDir = undefined;
outsideDir = undefined;
});
it('confines on the resolved path: a workspace symlink to an outside file is rejected', async () => {
// realpath so a symlinked tmpdir (e.g. macOS /var -> /private/var) can't skew containment checks
workspaceDir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'codeman-genart-ws-')));
outsideDir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'codeman-genart-out-')));
const outsideFile = join(outsideDir, 'private-notes.md');
await fs.writeFile(outsideFile, 'secret');
const linkPath = join(workspaceDir, 'x.md');
await fs.symlink(outsideFile, linkPath);
await expect(
registerGeneratedArtifactAttachment({ sessionId, filePath: linkPath, sessionWorkingDir: workspaceDir })
).rejects.toMatchObject({ statusCode: 403 });
});
it('registers a real workspace file', async () => {
workspaceDir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'codeman-genart-ws-')));
const filePath = join(workspaceDir, 'mockup.png');
await fs.writeFile(filePath, 'png-bytes');
const event = await registerGeneratedArtifactAttachment({
sessionId,
filePath,
sessionWorkingDir: workspaceDir,
});
expect(event.fileName).toBe('mockup.png');
expect(event.attachmentType).toBe('image');
});
});
});