analyze.test.mjs 4.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  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 { analyzeProductImages } from './analyze.mjs';
  7. const projectRoot = path.resolve(import.meta.dirname, '..', '..', '..', '..', '..');
  8. function tempWorkspace(prefix) {
  9. return fs.mkdtempSync(path.join(os.tmpdir(), `fmode-image-set-${prefix}-`));
  10. }
  11. test('analysis rejects missing current-request image without calling provider', async () => {
  12. let called = false;
  13. const result = await analyzeProductImages({ allowClaudeAttachments: false }, { provider: { analyze: async () => { called = true; } } });
  14. assert.equal(result.status, 'needs_input');
  15. assert.equal(called, false);
  16. });
  17. test('analysis validates structured Gemini result', async () => {
  18. const workspace = tempWorkspace('analysis');
  19. const fixture = path.join(workspace, 'analysis.png');
  20. fs.writeFileSync(fixture, Buffer.from([137, 80, 78, 71]));
  21. try {
  22. const provider = {
  23. analyze: async () => ({
  24. model: 'fake-gemini', usage: {}, finishReason: 'stop',
  25. analysis: {
  26. visible_facts: [{ field: 'color', value: 'red', source_image: 1, confidence: 0.9 }],
  27. visual_inferences: [], unknowns: ['size'],
  28. product_identity: { product_name: '红瓶精华', category: 'beauty', shape: 'bottle', packaging: 'box', logo: '', visible_text: [], materials: [], colors: ['red'], distinctive_features: [] },
  29. brand_style: { palette: ['#ff0000'], lighting: 'soft', mood: 'clean', composition: 'centered' },
  30. quality_risks: [], recommended_reference_roles: [{ image: 1, role: 'identity' }]
  31. }
  32. })
  33. };
  34. const result = await analyzeProductImages({ imagePath: fixture, workspaceRoot: workspace, cwd: workspace, allowClaudeAttachments: false }, { provider, workspaceRoot: workspace });
  35. assert.equal(result.status, 'ok');
  36. assert.equal(result.data.analysis.visible_facts[0].value, 'red');
  37. assert.equal(result.data.analysis.product_identity.product_name, '红瓶精华');
  38. } finally {
  39. fs.rmSync(workspace, { recursive: true, force: true });
  40. }
  41. });
  42. test('analysis requires a complete role mapping for multiple images and passes it to Gemini', async () => {
  43. const workspace = tempWorkspace('roles');
  44. const first = path.join(workspace, 'role-1.png');
  45. const second = path.join(workspace, 'role-2.png');
  46. fs.writeFileSync(first, Buffer.from([137, 80, 78, 71]));
  47. fs.writeFileSync(second, Buffer.from([137, 80, 78, 72]));
  48. let received;
  49. const provider = { analyze: async (_images, input) => {
  50. received = input.imageRoles;
  51. return { model: 'fake', usage: {}, finishReason: 'stop', analysis: {} };
  52. } };
  53. try {
  54. const incomplete = await analyzeProductImages({ images: [first, second], imageRoles: { 1: 'identity' }, workspaceRoot: workspace, cwd: workspace, allowClaudeAttachments: false }, { provider, workspaceRoot: workspace });
  55. assert.equal(incomplete.status, 'needs_input');
  56. const complete = await analyzeProductImages({ images: [first, second], imageRoles: { 1: 'identity', 2: 'detail' }, workspaceRoot: workspace, cwd: workspace, allowClaudeAttachments: false }, { provider, workspaceRoot: workspace });
  57. assert.equal(complete.status, 'ok');
  58. assert.deepEqual(received.map(item => item.role), ['identity', 'detail']);
  59. } finally {
  60. fs.rmSync(workspace, { recursive: true, force: true });
  61. }
  62. });
  63. test('analysis rejects a local image outside the declared workspace', async () => {
  64. const parent = tempWorkspace('outside-boundary');
  65. const workspace = path.join(parent, 'workspace');
  66. const outside = path.join(parent, 'outside.png');
  67. fs.mkdirSync(workspace, { recursive: true });
  68. fs.writeFileSync(outside, Buffer.from([137, 80, 78, 71]));
  69. try {
  70. const result = await analyzeProductImages({ imagePath: outside, workspaceRoot: workspace, cwd: workspace, allowClaudeAttachments: false }, { workspaceRoot: workspace, provider: { analyze: async () => ({ analysis: {} }) } });
  71. assert.equal(result.status, 'needs_input');
  72. assert.match(result.assistantMessage, /当前项目内/);
  73. } finally {
  74. fs.rmSync(parent, { recursive: true, force: true });
  75. }
  76. });