Forked from vadimfedenko/analyze-images
Forked from vadimfedenko/analyze-images
scripts / handoff-smoke.cjs
scripts / handoff-smoke.cjs
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { Chat, ChatMessage, FileHandle } = require('@lmstudio/sdk');
const { preprocess } = require('../dist/promptPreprocessor');
const { makeCandidate, recentCandidates } = require('../dist/attachmentIdentity');
(async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-handoff-'));
const otherRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-other-chat-'));
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64');
await fs.writeFile(path.join(root, 'image.png'), png);
await fs.writeFile(path.join(root, 'same-image.png'), png);
const receipt = await makeCandidate(root, 'image.png');
const makeHistory = (candidate = receipt, toolName = 'Analyze_Local_Image_Advanced') => Chat.from({messages: [
{role: 'assistant', content: [{type: 'toolCallRequest', toolCallRequest: {id: 'call-1', type: 'function', name: toolName, arguments: {imageName: 'image.png'}}}]},
{role: 'tool', content: [{type: 'toolCallResult', toolCallId: 'call-1', content: JSON.stringify({analysis: 'test analysis', _aiaImageCandidate: candidate})}]},
]});
let history = makeHistory();
let uploads = 0;
const lookup = new Map([['manual-a', path.join(root, 'image.png')], ['manual-b', path.join(root, 'same-image.png')]]);
const client = {files: {
getLocalFileAbsolutePath: async name => ({path: lookup.get(name) ?? path.join(root, name)}),
prepareImageBase64: async (name, data) => {
const bytes = Buffer.from(data, 'base64');
const id = `upload-${++uploads}`;
const target = path.join(root, id);
await fs.writeFile(target, bytes); lookup.set(id, target);
return new FileHandle(client.files, `local:${id}`, 'image', bytes.length, name);
},
}};
const settings = {autoAttach: false, carryAnalyzed: true, maxImages: 2, startImage: 1, pageUrl: ''};
const ctl = {
client, abortSignal: new AbortController().signal, guardAbort() {},
getWorkingDirectory: () => root,
getPluginConfig: () => ({get: key => settings[key]}),
pullHistory: async () => history,
createStatus: () => ({setState() {}, setText() {}}), debug() {},
};
const original = ChatMessage.from('Continue the conversation.');
const carried = await preprocess(ctl, original);
assert.equal(carried.getFiles(client).length, 1);
assert.equal(original.getFiles(client).length, 0);
assert.equal(uploads, 0);
history.append(carried);
assert.equal(await preprocess(ctl, original), original); // Successful delivery visible in history.
assert.equal(uploads, 0);
history = Chat.from([]); // Different chat, SAME working directory.
assert.equal(await preprocess(ctl, original), original);
history = makeHistory();
const mismatchedRoot = {...ctl, getWorkingDirectory: () => otherRoot};
assert.equal(await preprocess(mismatchedRoot, original), original);
settings.carryAnalyzed = false;
assert.equal(await preprocess(ctl, original), original);
settings.carryAnalyzed = true;
const manual = ChatMessage.from({role: 'user', content: [
{type: 'text', text: 'Manually retry this image.'},
{type: 'file', identifier: 'local:manual-a', name: 'one.png', sizeBytes: png.length, fileType: 'image'},
{type: 'file', identifier: 'local:manual-b', name: 'different-name.png', sizeBytes: png.length, fileType: 'image'},
]}).asImmutableCopy();
const organized = await preprocess(ctl, manual);
assert.equal(organized.getFiles(client).length, 1);
assert.equal(manual.getFiles(client).length, 2);
assert.equal(uploads, 0); // Manual image wins over the pending candidate.
history.append(organized);
const repeatedManual = await preprocess(ctl, manual);
assert.equal(repeatedManual.getFiles(client).length, 1); // Explicit manual reattachment is preserved.
assert.equal(uploads, 0);
history = makeHistory();
await fs.rename(path.join(root, 'image.png'), path.join(root, 'held.png'));
assert.equal(await preprocess(ctl, original), original);
await fs.rename(path.join(root, 'held.png'), path.join(root, 'image.png'));
assert.equal((await preprocess(ctl, original)).getFiles(client).length, 1); // Failure did not consume receipt.
await fs.writeFile(path.join(root, 'image.png'), Buffer.concat([png, Buffer.from('changed')]));
assert.equal(await preprocess(ctl, original), original); // Changed target is not silently substituted.
await fs.writeFile(path.join(root, 'image.png'), png);
assert.equal((await preprocess(ctl, original)).getFiles(client).length, 1);
history = makeHistory({...receipt, relativePath: '../outside.png'});
assert.equal(await preprocess(ctl, original), original);
history = makeHistory({...receipt, createdAt: Date.now() - 25 * 60 * 60 * 1000});
assert.equal(recentCandidates(history).length, 0);
history = makeHistory(receipt, 'Some_Other_Tool');
assert.equal(recentCandidates(history).length, 0);
history = Chat.from([{role: 'user', content: JSON.stringify({_aiaImageCandidate: receipt})}]);
assert.equal(recentCandidates(history).length, 0);
history = makeHistory();
const missingManual = ChatMessage.from({role: 'user', content: [
{type: 'text', text: 'Retry'},
{type: 'file', identifier: 'local:missing', name: 'missing.png', sizeBytes: 1, fileType: 'image'},
]});
const before = uploads;
assert.equal(await preprocess(ctl, missingManual), missingManual);
assert.equal(uploads, before);
console.log('PASS: history-local handoff, one-time delivery, same-directory chat separation, root verification, manual byte-level dedup/priority/retry, failure retry, changed file, traversal, expiry, unrelated tool/user data and unreadable manual fallback.');
console.log('Controller/model are mocked; real SDK inline attachments and filesystem are used, with no temporary uploads.');
})().catch(error => {console.error(error); process.exitCode = 1;});
const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
const { Chat, ChatMessage, FileHandle } = require('@lmstudio/sdk');
const { preprocess } = require('../dist/promptPreprocessor');
const { makeCandidate, recentCandidates } = require('../dist/attachmentIdentity');
(async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-handoff-'));
const otherRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'aia-other-chat-'));
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64');
await fs.writeFile(path.join(root, 'image.png'), png);
await fs.writeFile(path.join(root, 'same-image.png'), png);
const receipt = await makeCandidate(root, 'image.png');
const makeHistory = (candidate = receipt, toolName = 'Analyze_Local_Image_Advanced') => Chat.from({messages: [
{role: 'assistant', content: [{type: 'toolCallRequest', toolCallRequest: {id: 'call-1', type: 'function', name: toolName, arguments: {imageName: 'image.png'}}}]},
{role: 'tool', content: [{type: 'toolCallResult', toolCallId: 'call-1', content: JSON.stringify({analysis: 'test analysis', _aiaImageCandidate: candidate})}]},
]});
let history = makeHistory();
let uploads = 0;
const lookup = new Map([['manual-a', path.join(root, 'image.png')], ['manual-b', path.join(root, 'same-image.png')]]);
const client = {files: {
getLocalFileAbsolutePath: async name => ({path: lookup.get(name) ?? path.join(root, name)}),
prepareImageBase64: async (name, data) => {
const bytes = Buffer.from(data, 'base64');
const id = `upload-${++uploads}`;
const target = path.join(root, id);
await fs.writeFile(target, bytes); lookup.set(id, target);
return new FileHandle(client.files, `local:${id}`, 'image', bytes.length, name);
},
}};
const settings = {autoAttach: false, carryAnalyzed: true, maxImages: 2, startImage: 1, pageUrl: ''};
const ctl = {
client, abortSignal: new AbortController().signal, guardAbort() {},
getWorkingDirectory: () => root,
getPluginConfig: () => ({get: key => settings[key]}),
pullHistory: async () => history,
createStatus: () => ({setState() {}, setText() {}}), debug() {},
};
const original = ChatMessage.from('Continue the conversation.');
const carried = await preprocess(ctl, original);
assert.equal(carried.getFiles(client).length, 1);
assert.equal(original.getFiles(client).length, 0);
assert.equal(uploads, 0);
history.append(carried);
assert.equal(await preprocess(ctl, original), original); // Successful delivery visible in history.
assert.equal(uploads, 0);
history = Chat.from([]); // Different chat, SAME working directory.
assert.equal(await preprocess(ctl, original), original);
history = makeHistory();
const mismatchedRoot = {...ctl, getWorkingDirectory: () => otherRoot};
assert.equal(await preprocess(mismatchedRoot, original), original);
settings.carryAnalyzed = false;
assert.equal(await preprocess(ctl, original), original);
settings.carryAnalyzed = true;
const manual = ChatMessage.from({role: 'user', content: [
{type: 'text', text: 'Manually retry this image.'},
{type: 'file', identifier: 'local:manual-a', name: 'one.png', sizeBytes: png.length, fileType: 'image'},
{type: 'file', identifier: 'local:manual-b', name: 'different-name.png', sizeBytes: png.length, fileType: 'image'},
]}).asImmutableCopy();
const organized = await preprocess(ctl, manual);
assert.equal(organized.getFiles(client).length, 1);
assert.equal(manual.getFiles(client).length, 2);
assert.equal(uploads, 0); // Manual image wins over the pending candidate.
history.append(organized);
const repeatedManual = await preprocess(ctl, manual);
assert.equal(repeatedManual.getFiles(client).length, 1); // Explicit manual reattachment is preserved.
assert.equal(uploads, 0);
history = makeHistory();
await fs.rename(path.join(root, 'image.png'), path.join(root, 'held.png'));
assert.equal(await preprocess(ctl, original), original);
await fs.rename(path.join(root, 'held.png'), path.join(root, 'image.png'));
assert.equal((await preprocess(ctl, original)).getFiles(client).length, 1); // Failure did not consume receipt.
await fs.writeFile(path.join(root, 'image.png'), Buffer.concat([png, Buffer.from('changed')]));
assert.equal(await preprocess(ctl, original), original); // Changed target is not silently substituted.
await fs.writeFile(path.join(root, 'image.png'), png);
assert.equal((await preprocess(ctl, original)).getFiles(client).length, 1);
history = makeHistory({...receipt, relativePath: '../outside.png'});
assert.equal(await preprocess(ctl, original), original);
history = makeHistory({...receipt, createdAt: Date.now() - 25 * 60 * 60 * 1000});
assert.equal(recentCandidates(history).length, 0);
history = makeHistory(receipt, 'Some_Other_Tool');
assert.equal(recentCandidates(history).length, 0);
history = Chat.from([{role: 'user', content: JSON.stringify({_aiaImageCandidate: receipt})}]);
assert.equal(recentCandidates(history).length, 0);
history = makeHistory();
const missingManual = ChatMessage.from({role: 'user', content: [
{type: 'text', text: 'Retry'},
{type: 'file', identifier: 'local:missing', name: 'missing.png', sizeBytes: 1, fileType: 'image'},
]});
const before = uploads;
assert.equal(await preprocess(ctl, missingManual), missingManual);
assert.equal(uploads, before);
console.log('PASS: history-local handoff, one-time delivery, same-directory chat separation, root verification, manual byte-level dedup/priority/retry, failure retry, changed file, traversal, expiry, unrelated tool/user data and unreadable manual fallback.');
console.log('Controller/model are mocked; real SDK inline attachments and filesystem are used, with no temporary uploads.');
})().catch(error => {console.error(error); process.exitCode = 1;});