Skip to content

tests/operations.test.js

126 lines · 8.0 KB · JavaScript
Raw
import test from 'node:test';
import assert from 'node:assert/strict';
import { createOperations } from '../operations.js';
const file = { name: 'fixture.png', base64: 'YWJj' };
function sample(schema) {
  if (Array.isArray(schema.type)) return sample({ ...schema, type: schema.type[0] });
  if (schema.const !== undefined) return schema.const;
  if (schema.enum) return schema.enum[0];
  if (schema.anyOf) return sample(schema.anyOf[0]);
  if (schema.pattern?.includes('d{4}')) return '2026-09-22';
  if (schema.type === 'object') return Object.fromEntries((schema.required ?? []).map(k => [k, sample(schema.properties[k])]));
  if (schema.type === 'array') return [sample(schema.items)];
  if (schema.type === 'number' || schema.type === 'integer') return 1;
  if (schema.type === 'boolean') return false;
  if (schema.type === 'string') return 'fixture';
  return {};
}
function setup() {
  const requests = [], uploads = [];
  const client = {
    resolveFrameId() { throw new Error('Writes must not auto-discover frames'); },
    async request(method, path, options) {
      requests.push({ method, path, options });
      if (path.endsWith('cloud_upload_credentials')) return { data: { credentials: { access_key_id: 'test', secret_access_key: 'test', session_token: 'test' }, region: 'us-east-1', bucket: 'test-bucket', key_prefix: 'test/' } };
      if (method === 'GET' && path.endsWith('/list_items')) return { data: [{ id: 'fixture', type: 'item', attributes: {} }] };
      return { data: { id: 'fixture', type: 'fixture', attributes: {}, message_ids: ['fixture'] } };
    },
  };
  return { dispatch: createOperations(client, {}, async params => { uploads.push(params); return 'test-etag'; }), requests, uploads };
}
function argsFor(tool) {
  const args = sample(tool.inputSchema);
  if (tool.inputSchema.properties.frameId) args.frameId = 'frame-test';
  if (tool.inputSchema.properties.file) args.file = file;
  if (tool.name === 'skylight_link_apple_calendar') args.app_specific_password = '{{secret:appleAppPassword}}';
  return args;
}
test('All 114 upstream operations, with 77 writes, are discoverable with schemas', async () => {
  const { dispatch } = setup();
  const tools = await dispatch({ operation: 'tools' });
  assert.equal(tools.length, 114);
  assert.equal(tools.filter(t => !t.readOnly).length, 77);
  assert.equal(new Set(tools.map(t => t.name)).size, 114);
  assert.ok(tools.every(t => t.inputSchema.type === 'object'));
});
test('Every write previews without any API request or upload, including inner confirm flags', async () => {
  const { dispatch, requests, uploads } = setup();
  for (const tool of await dispatch({ operation: 'tools', mode: 'write' })) {
    const args = argsFor(tool);
    if (tool.inputSchema.properties.confirm) args.confirm = true;
    const preview = await dispatch({ operation: tool.name, args });
    assert.equal(preview.dryRun, true, tool.name);
    assert.match(preview.previewToken, /^[0-9a-f]{64}$/);
    assert.equal(requests.length, 0, tool.name);
    assert.equal(uploads.length, 0, tool.name);
  }
});
test('Confirmed writes exercise every upstream handler against the mock API', async () => {
  for (const tool of await setup().dispatch({ operation: 'tools', mode: 'write' })) {
    const { dispatch, requests, uploads } = setup();
    const args = argsFor(tool);
    const preview = await dispatch({ operation: tool.name, args });
    try { await dispatch({ operation: tool.name, args, confirm: true, previewToken: preview.previewToken }); }
    catch (error) { throw new Error(tool.name + ': ' + error.message); }
    assert.ok(requests.some(r => r.method !== 'GET') || uploads.length > 0, tool.name);
  }
});
test('Wrong/missing preview token, changed args, implicit frames and unknown fields cannot write', async () => {
  const { dispatch, requests } = setup();
  const operation = 'skylight_delete_chore';
  const args = { id: 'chore-test', frameId: 'frame-test', apply_to: 'all' };
  const preview = await dispatch({ operation, args });
  await assert.rejects(dispatch({ operation, args, confirm: true }), /previewToken/);
  await assert.rejects(dispatch({ operation, args: { ...args, id: 'other' }, confirm: true, previewToken: preview.previewToken }), /previewToken/);
  await assert.rejects(dispatch({ operation: 'skylight_create_chore', args: { summary: 'Test', category_id: 'member' } }), /frameId/);
  await assert.rejects(dispatch({ operation, args: { ...args, surprise: true } }));
  const dry = await dispatch({ operation, args, confirm: true, previewToken: preview.previewToken, dryRun: true });
  assert.equal(dry.dryRun, true); assert.equal(dry.bulk, true);
  assert.equal(requests.length, 0);
});
test('Representative edits preserve exact method, endpoint and body through upstream code', async () => {
  const examples = [
    ['skylight_update_category', { id: 'member', color: '#123456' }, 'PUT', '/frames/frame-test/categories/member', { color: '#123456' }],
    ['skylight_create_chore', { summary: 'Make bed', category_id: 'member' }, 'POST', '/frames/frame-test/chores', { summary: 'Make bed', category_id: 'member' }],
    ['skylight_update_event', { id: 'event', category_ids: ['member'] }, 'PUT', '/frames/frame-test/calendar_events/event', { category_ids: ['member'] }],
    ['skylight_delete_event', { id: 'event' }, 'DELETE', '/frames/frame-test/calendar_events/event', undefined],
  ];
  for (const [operation, partial, method, path, body] of examples) {
    const { dispatch, requests } = setup(); const args = { ...partial, frameId: 'frame-test' };
    const p = await dispatch({ operation, args });
    await dispatch({ operation, args, confirm: true, previewToken: p.previewToken });
    assert.equal(requests[0].method, method); assert.equal(requests[0].path, path); assert.deepEqual(requests[0].options?.body, body);
  }
});
test('File uploads use bytes, redact preview payloads, and keep temporary credentials out of results', async () => {
  const { dispatch, requests, uploads } = setup();
  const args = { file, frameId: 'frame-test' }, operation = 'skylight_upload_photo';
  const p = await dispatch({ operation, args });
  assert.equal(p.args.file.bytes, 3); assert.equal(p.args.file.base64, undefined);
  const r = await dispatch({ operation, args, confirm: true, previewToken: p.previewToken });
  assert.equal(uploads[0].body.toString(), 'abc');
  assert.equal(requests.at(-1).path, '/messages/uploads');
  assert.ok(!JSON.stringify(r).includes('secret_access_key'));
  await assert.rejects(dispatch({ operation, args: { ...args, file: { name: '../secrets.txt', base64: 'YWJj' } } }));
});
test('Apple password requires a stored secret and previews never echo a resolvable placeholder', async () => {
  const { dispatch } = setup(); const operation = 'skylight_link_apple_calendar';
  await assert.rejects(dispatch({ operation, args: { frameId: 'frame-test', email: 'test@example.com', app_specific_password: 'plaintext' } }), /Kody secret/);
  const p = await dispatch({ operation, args: { frameId: 'frame-test', email: 'test@example.com', app_specific_password: '{{secret:appleAppPassword}}' } });
  assert.equal(p.args.app_specific_password, '[stored credential]');
});
test('Clearing a list aborts if its reviewed membership changed', async () => {
  const { dispatch, requests } = setup();
  const operation = 'skylight_clear_list', args = { frameId: 'frame-test', listId: 'list-test', item_ids: ['old-item'] };
  const p = await dispatch({ operation, args });
  await assert.rejects(dispatch({ operation, args, confirm: true, previewToken: p.previewToken }), /contents changed/);
  assert.ok(requests.every(r => r.method === 'GET'));
});
test('Avatar transport sends supplied bytes in multipart form without reading local files', async () => {
  const { dispatch, requests } = setup();
  const operation = 'skylight_set_member_avatar', args = { frameId: 'frame-test', id: 'member', file };
  const p = await dispatch({ operation, args });
  await dispatch({ operation, args, confirm: true, previewToken: p.previewToken });
  assert.equal(requests[0].method, 'PUT');
  assert.equal(await requests[0].options.formData.get('profile_picture').text(), 'abc');
});