| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 |
- import assert from 'node:assert/strict';
- import fs from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- import test from 'node:test';
- import { resolveClaudeAttachments } from './attachment-resolver.mjs';
- import { createRequestKey } from './request-key.mjs';
- import { isInside, resolveOutputRoot, resolveRunDirectory } from './run-paths.mjs';
- import { readState, writeState } from './state-store.mjs';
- const projectRoot = path.resolve(import.meta.dirname, '..', '..', '..', '..');
- function tempDirectory(prefix) {
- return fs.mkdtempSync(path.join(os.tmpdir(), `fmode-image-set-${prefix}-`));
- }
- test('request key is deterministic', () => {
- const a = createRequestKey({ sessionId: 's', promptId: 'p', action: 'generate', planHash: 'h', extra: { b: 2, a: 1 } });
- const b = createRequestKey({ sessionId: 's', promptId: 'p', action: 'generate', planHash: 'h', extra: { a: 1, b: 2 } });
- assert.equal(a, b);
- });
- test('run paths reject project escape and skill source outputs', () => {
- const packageRoot = path.join(projectRoot, 'fmode-image-set');
- assert.throws(() => resolveOutputRoot({ workspaceRoot: projectRoot, outputRoot: path.resolve(projectRoot, '..', 'escape') }), /固定/);
- assert.throws(() => resolveOutputRoot({ workspaceRoot: projectRoot, outputRoot: path.join(projectRoot, 'custom-output') }), /固定/);
- assert.throws(() => resolveOutputRoot({ workspaceRoot: packageRoot, packageRoot }), /Skill 源码/);
- const paths = resolveRunDirectory({ workspaceRoot: projectRoot, packageRoot, runId: 'run_abcdef' });
- assert.equal(isInside(projectRoot, paths.runDirectory), true);
- });
- test('Claude attachment filters require exact session, prompt, and cwd fields', () => {
- const config = tempDirectory('claude-strict');
- const logs = path.join(config, 'projects', 'fixture');
- fs.mkdirSync(logs, { recursive: true });
- const records = [
- { timestamp: '2026-01-01T00:00:00Z', cwd: projectRoot, message: { id: 'p1', role: 'user', content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'AAAA' } }] } },
- { timestamp: '2026-01-01T00:01:00Z', sessionId: 's1', message: { id: 'p1', role: 'user', content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'BBBB' } }] } }
- ];
- fs.writeFileSync(path.join(logs, 's1.jsonl'), records.map(JSON.stringify).join('\n'));
- try {
- assert.equal(resolveClaudeAttachments({ claudeConfigDir: config, sessionId: 's1', promptId: 'p1', cwd: projectRoot }).length, 0);
- } finally { fs.rmSync(config, { recursive: true, force: true }); }
- });
- test('state checksum detects tampering', () => {
- const run = tempDirectory('state');
- try {
- writeState(run, { status: 'planned', value: 1 });
- assert.equal(readState(run).value, 1);
- const file = path.join(run, 'state.json');
- const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
- doc.payload.value = 2;
- fs.writeFileSync(file, JSON.stringify(doc));
- assert.throws(() => readState(run), /完整性/);
- } finally { fs.rmSync(run, { recursive: true, force: true }); }
- });
- test('latest Claude user request with no image never reuses older attachment', () => {
- const config = tempDirectory('claude');
- const logs = path.join(config, 'projects', 'fixture');
- fs.mkdirSync(logs, { recursive: true });
- const records = [
- { timestamp: '2026-01-01T00:00:00Z', sessionId: 's1', cwd: projectRoot, message: { id: 'p1', role: 'user', content: [{ type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'AAAA' } }] } },
- { timestamp: '2026-01-01T00:01:00Z', sessionId: 's1', cwd: projectRoot, message: { id: 'p2', role: 'user', content: [{ type: 'text', text: 'no image' }] } }
- ];
- fs.writeFileSync(path.join(logs, 's1.jsonl'), records.map(JSON.stringify).join('\n'));
- try {
- const current = resolveClaudeAttachments({ claudeConfigDir: config, sessionId: 's1', cwd: projectRoot });
- assert.equal(current.length, 0);
- const exact = resolveClaudeAttachments({ claudeConfigDir: config, sessionId: 's1', promptId: 'p1', cwd: projectRoot });
- assert.equal(exact.length, 1);
- } finally { fs.rmSync(config, { recursive: true, force: true }); }
- });
|