core.test.mjs 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. import assert from 'node:assert/strict';
  2. import fs from 'node:fs';
  3. import os from 'node:os';
  4. import path from 'node:path';
  5. import test from 'node:test';
  6. import { resolveClaudeAttachments } from './attachment-resolver.mjs';
  7. import { createRequestKey } from './request-key.mjs';
  8. import { isInside, resolveOutputRoot, resolveRunDirectory } from './run-paths.mjs';
  9. import { readState, writeState } from './state-store.mjs';
  10. const projectRoot = path.resolve(import.meta.dirname, '..', '..', '..', '..');
  11. function tempDirectory(prefix) {
  12. return fs.mkdtempSync(path.join(os.tmpdir(), `fmode-image-set-${prefix}-`));
  13. }
  14. test('request key is deterministic', () => {
  15. const a = createRequestKey({ sessionId: 's', promptId: 'p', action: 'generate', planHash: 'h', extra: { b: 2, a: 1 } });
  16. const b = createRequestKey({ sessionId: 's', promptId: 'p', action: 'generate', planHash: 'h', extra: { a: 1, b: 2 } });
  17. assert.equal(a, b);
  18. });
  19. test('run paths reject project escape and skill source outputs', () => {
  20. const packageRoot = path.join(projectRoot, 'fmode-image-set');
  21. assert.throws(() => resolveOutputRoot({ workspaceRoot: projectRoot, outputRoot: path.resolve(projectRoot, '..', 'escape') }), /固定/);
  22. assert.throws(() => resolveOutputRoot({ workspaceRoot: projectRoot, outputRoot: path.join(projectRoot, 'custom-output') }), /固定/);
  23. assert.throws(() => resolveOutputRoot({ workspaceRoot: packageRoot, packageRoot }), /Skill 源码/);
  24. const paths = resolveRunDirectory({ workspaceRoot: projectRoot, packageRoot, runId: 'run_abcdef' });
  25. assert.equal(isInside(projectRoot, paths.runDirectory), true);
  26. });
  27. test('Claude attachment filters require exact session, prompt, and cwd fields', () => {
  28. const config = tempDirectory('claude-strict');
  29. const logs = path.join(config, 'projects', 'fixture');
  30. fs.mkdirSync(logs, { recursive: true });
  31. const records = [
  32. { 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' } }] } },
  33. { 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' } }] } }
  34. ];
  35. fs.writeFileSync(path.join(logs, 's1.jsonl'), records.map(JSON.stringify).join('\n'));
  36. try {
  37. assert.equal(resolveClaudeAttachments({ claudeConfigDir: config, sessionId: 's1', promptId: 'p1', cwd: projectRoot }).length, 0);
  38. } finally { fs.rmSync(config, { recursive: true, force: true }); }
  39. });
  40. test('state checksum detects tampering', () => {
  41. const run = tempDirectory('state');
  42. try {
  43. writeState(run, { status: 'planned', value: 1 });
  44. assert.equal(readState(run).value, 1);
  45. const file = path.join(run, 'state.json');
  46. const doc = JSON.parse(fs.readFileSync(file, 'utf8'));
  47. doc.payload.value = 2;
  48. fs.writeFileSync(file, JSON.stringify(doc));
  49. assert.throws(() => readState(run), /完整性/);
  50. } finally { fs.rmSync(run, { recursive: true, force: true }); }
  51. });
  52. test('latest Claude user request with no image never reuses older attachment', () => {
  53. const config = tempDirectory('claude');
  54. const logs = path.join(config, 'projects', 'fixture');
  55. fs.mkdirSync(logs, { recursive: true });
  56. const records = [
  57. { 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' } }] } },
  58. { timestamp: '2026-01-01T00:01:00Z', sessionId: 's1', cwd: projectRoot, message: { id: 'p2', role: 'user', content: [{ type: 'text', text: 'no image' }] } }
  59. ];
  60. fs.writeFileSync(path.join(logs, 's1.jsonl'), records.map(JSON.stringify).join('\n'));
  61. try {
  62. const current = resolveClaudeAttachments({ claudeConfigDir: config, sessionId: 's1', cwd: projectRoot });
  63. assert.equal(current.length, 0);
  64. const exact = resolveClaudeAttachments({ claudeConfigDir: config, sessionId: 's1', promptId: 'p1', cwd: projectRoot });
  65. assert.equal(exact.length, 1);
  66. } finally { fs.rmSync(config, { recursive: true, force: true }); }
  67. });