DEV Community

Rahul
Rahul

Posted on

Testcraft Studio frontend 17: src/chatAssistant.js

import { contextKey, eligibleDocuments, resolveSelection, timestamp, uid } from './store.jsx';
import { makeDemoSuite } from './generation.js';

export const testKinds = ['Auto', 'Technical', 'Functional', 'Business'];
const stopWords = new Set('a an the and or to for of in on at by with from about against based using please can could would will should what which where when why how does do is are was were be it its this that these those my our your me us tell show find give explain answer answers information knowledge source sources document documents evidence requirement requirements test tests testing testcase testcases case cases scenario scenarios generate generated generating create build prepare write draft provide add include more refine focus only technical functional business regression positive negative boundary all any selected uploaded current workspace system'.split(' '));
const tokens = value => [...new Set((value.toLowerCase().match(/[a-z0-9_-]+/g) || []).filter(word => word.length > 2 && !stopWords.has(word)).map(word => word.replace(/s$/, '')))];
const score = (text, words) => words.filter(word => text.toLowerCase().includes(word)).length;
const negative = text => /\b(reject|invalid|missing|absent|exceed|fail|prevent|disallow|denied)\w*\b|must not|not allowed/i.test(text);
const positive = text => /\b(accept|allow|success|succeed)\w*\b/i.test(text);

export function interpretChatRequest(prompt, { kind = 'Auto', previousDraft } = {}) {
  const text = prompt.trim();
  const asksSources = /\b(source|sources|citation|citations|reference|references|where.*document|where.*rule|evidence for)\b/i.test(text);
  const asksTests = /\b(test\s*cases?|tests|scenarios?)\b/i.test(text);
  const creates = /\b(generate|create|build|prepare|write|draft|give|provide)\b/i.test(text) && asksTests;
  const followsUp = !!previousDraft && /^(?:please\s+)?(?:add|include|refine|focus|only|make|change|regenerate|more|keep|remove|use)\b/i.test(text);
  const intent = creates ? 'generate' : followsUp && !asksSources ? 'refine' : asksSources ? 'sources' : 'question';
  const explicitKind = /\b(technical|database|sql|api|procedure|integration)\b/i.test(text) ? 'Technical'
    : /\b(functional|functionality|behaviour|behavior)\b/i.test(text) ? 'Functional'
      : /\b(business|uat|acceptance)\b/i.test(text) ? 'Business' : '';
  const selectedKind = explicitKind || (kind !== 'Auto' ? kind : intent === 'refine' ? previousDraft?.kind : '') || 'Functional';
  const scenario = /\bnegative\b/i.test(text) ? 'Negative' : /\bpositive\b/i.test(text) ? 'Positive' : /\bregression\b/i.test(text) ? 'Regression' : '';
  const words = tokens(text).filter(word => !/^(?:\d+|cr-\d+|sql|api|database|procedure|integration|uat|acceptance)$/.test(word));
  const crRefs = [...new Set(text.toUpperCase().match(/CR-\d+/g) || [])];
  const count = Number(text.match(/\b(?:generate|create|give|provide|write|prepare)\s+(\d+)\b/i)?.[1]) || null;
  return { intent, kind: selectedKind, scenario, words, crRefs, count: Number.isSafeInteger(count) && count > 0 ? count : null, append: /^\s*(?:please\s+)?(?:add|include|more)\b/i.test(text) };
}

function contextSnapshot(state, workspace) {
  if (!state.session.authenticated || workspace.team !== state.session.team) throw new Error('Open a workspace belonging to your signed-in team.');
  const baseline = state.baselines.find(item => item.id === workspace.baselineId);
  return {
    workspaceId: workspace.id, version: workspace.version, team: workspace.team,
    baselineId: workspace.baselineId, baselineVersion: baseline?.version || '', baselineStatus: baseline?.status || '',
    crs: workspace.crIds.map(id => { const cr = state.crs.find(item => item.id === id); return { id, revision: cr?.revision }; }),
    sharedContractIds: [...workspace.sharedContractIds || []].sort(),
    rules: [...(baseline?.rules || []), ...(workspace.customRules || [])].map(rule => ({ id: rule.id, text: rule.text, source: rule.source })),
    crChanges: state.crs.filter(cr => workspace.crIds.includes(cr.id)).flatMap(cr => cr.changes),
    sources: eligibleDocuments(state, workspace).map(doc => ({ id: doc.id, revision: doc.revision, checksum: doc.checksum || '', status: doc.status, text: doc.excerpt || '' })).sort((a, b) => a.id.localeCompare(b.id)),
  };
}

export function latestChatDraft(messages = []) {
  return [...messages].reverse().find(message => message.role === 'assistant' && message.draft)?.draft;
}

export function buildChatRequest(state, workspace, { prompt, kind = 'Auto', templateId, messages = [] }) {
  if (!prompt.trim()) throw new Error('Enter a question or describe the test cases you need.');
  const previousDraft = latestChatDraft(messages);
  const interpretation = interpretChatRequest(prompt, { kind, previousDraft });
  const context = contextSnapshot(state, workspace);
  if (interpretation.intent === 'refine' && JSON.stringify(previousDraft.contextSnapshot) !== JSON.stringify(context)) {
    throw new Error('Workspace knowledge changed. Generate a new draft before refining these cases.');
  }
  return {
    id: uid('chat-request'), prompt: prompt.trim(), context,
    threadKey: contextKey(workspace), templateId, ...interpretation,
    previousDraft: interpretation.intent === 'refine' ? structuredClone(previousDraft) : null,
    history: messages.slice(-12).map(message => ({ role: message.role, content: message.content })),
  };
}

export function assertChatContext(state, request) {
  const workspace = state.workspaces.find(item => item.id === request.context.workspaceId);
  if (!workspace || state.activeWorkspaceId !== workspace.id || JSON.stringify(contextSnapshot(state, workspace)) !== JSON.stringify(request.context)) {
    throw new Error('Workspace knowledge changed. Send your request again in the current context.');
  }
  return workspace;
}

export function sourceCitation(doc, quote) {
  return { id: doc.id, name: doc.name, revision: doc.revision, checksum: doc.checksum || '', team: doc.team,
    locator: doc.locator || 'Uploaded document excerpt', excerpt: doc.excerpt || '', quote: quote || '' };
}

function resolvedRules(state, workspace) {
  const selection = resolveSelection(state, workspace.baselineId, workspace.crIds);
  const rules = new Map((selection.baseline?.rules || []).map(rule => [rule.id, rule]));
  selection.diff.forEach(change => { if (change.op === 'REMOVE') rules.delete(change.ruleId); else rules.set(change.ruleId, { id: change.ruleId, title: change.title, text: change.after, source: change.source }); });
  (workspace.customRules || []).forEach(rule => rules.set(rule.id, rule));
  return [...rules.values()];
}

function answerQuestion(state, workspace, request) {
  const docs = eligibleDocuments(state, workspace).filter(doc => ['Ready', 'Reviewed locally'].includes(doc.status));
  const previous = [...request.history].reverse().find(message => message.role === 'user')?.content || '';
  const words = request.words.length ? request.words : tokens(previous);
  const priorMessages = state.threads[request.threadKey]?.messages || [];
  const priorAnswer = [...priorMessages].reverse().find(message => message.role === 'assistant' && (message.citations?.length || message.sourceIds?.length));
  const wantsPriorSources = request.intent === 'sources' && (!request.words.length || /\b(these|those|this|that|above|previous|result|cases)\b/i.test(request.prompt));
  const priorIds = new Set(priorAnswer?.citations?.map(citation => citation.id) || priorAnswer?.sourceIds || []);
  let hits = docs.map(doc => ({ doc, score: score(`${doc.name} ${doc.excerpt || ''}`, words) }));
  if (wantsPriorSources && priorIds.size) hits = hits.filter(hit => priorIds.has(hit.doc.id));
  else if (words.length) hits = hits.filter(hit => hit.score > 0);
  else if (request.intent !== 'sources') hits = [];
  hits.sort((a, b) => b.score - a.score);
  if (!hits.length) return { content: 'I could not find supporting information in the selected workspace documents. Try a more specific question or attach a relevant document.', citations: [], suggestions: ['Show sources for the selected knowledge'] };
  const citations = hits.slice(0, 3).map(({ doc }) => {
    const segments = (doc.excerpt || '').split(/(?<=[.!?])\s+|\n{2,}/).map(text => text.trim()).filter(Boolean);
    const quote = segments.map(text => ({ text, score: score(text, words) })).sort((a, b) => b.score - a.score).slice(0, 2).map(item => item.text).join(' ');
    return sourceCitation(doc, quote.slice(0, 1400));
  });
  if (request.intent === 'sources') return {
    content: wantsPriorSources && priorIds.size ? 'These are the documents supporting the previous answer. Each reference keeps the cited revision and location.' : 'These references support the selected workspace knowledge. Open a source to inspect its text and revision.',
    citations, suggestions: ['Generate technical test cases', 'Generate functional test cases'],
  };
  const supported = resolvedRules(state, workspace).filter(rule => citations.some(doc => doc.id === rule.source) && score(`${rule.title} ${rule.text}`, words) > 0).slice(0, 3);
  const answer = supported.length
    ? supported.map(rule => `${rule.id} ยท ${rule.title}\n${rule.text}`).join('\n\n')
    : citations.filter(citation => citation.quote).map((citation, index) => `[${index + 1}] ${citation.quote}`).join('\n\n');
  return {
    content: answer || 'The source is available, but its text has not been extracted. Open the original document to inspect it or upload a readable technical or requirements document.',
    citations, suggestions: ['Show sources for this answer', 'Generate functional test cases'],
  };
}

function generateCases(state, workspace, request) {
  let suite;
  try { suite = makeDemoSuite(state, workspace, request.templateId); }
  catch (error) { return { content: error.message, citations: [], needsEvidence: true, suggestions: ['Show sources for the selected knowledge'] }; }
  const docs = eligibleDocuments(state, workspace).filter(doc => ['Ready', 'Reviewed locally'].includes(doc.status));
  const docMap = new Map(docs.map(doc => [doc.id, doc]));
  const previous = request.previousDraft;
  const words = request.words.length ? request.words : previous?.requestScope?.words || [];
  const crRefs = request.crRefs.length ? request.crRefs : previous?.requestScope?.crRefs || [];
  const selectedCrs = state.crs.filter(cr => workspace.crIds.includes(cr.id));
  if (crRefs.some(ref => !selectedCrs.some(cr => cr.ref === ref))) return { content: 'That change request is not in the selected workspace knowledge. Apply its revision in Knowledge versions, then generate again.', citations: [], needsEvidence: true };
  const supportsKind = test => test.sourceIds.every(id => {
    const doc = docMap.get(id);
    if (!doc?.excerpt?.trim()) return false;
    return request.kind === 'Technical' ? doc.category === 0 : request.kind === 'Business' ? doc.category === 1 || doc.category === 2 : true;
  });
  let tests = suite.tests.filter(supportsKind).filter(test => !words.length || score(`${test.title} ${test.expected} ${test.requirement}`, words) > 0);
  if (crRefs.length) tests = tests.filter(test => selectedCrs.some(cr => crRefs.includes(cr.ref) && cr.changes.some(change => change.ruleId === test.requirement)));
  tests = tests.map(test => {
    const doc = docMap.get(test.sourceIds[0]);
    const category = negative(test.expected) ? 'Negative' : positive(test.expected) ? 'Positive' : 'Regression';
    return { ...test, type: request.kind, category,
      objective: `Verify ${test.requirement} against the selected ${request.kind.toLowerCase()} evidence.`,
      preconditions: `The approved requirement and its source revision are available. Use the environment and fixtures defined in ${doc.name}.`,
      steps: [
        `Review ${test.requirement} in ${doc.name}${doc.locator ? `, ${doc.locator}` : ''}.`,
        category === 'Negative' ? 'Exercise the documented rejection or invalid-input condition using the source-defined fixture.' : `Exercise the documented ${request.kind === 'Technical' ? 'technical validation path' : request.kind === 'Business' ? 'business workflow' : 'application behavior'} using a source-defined fixture.`,
        'Compare the observed result with the cited expected behavior and record the evidence.',
      ],
      data: 'Use the input values and fixtures defined by the cited source; unspecified values require user input.',
      rationale: `Source-backed ${request.kind.toLowerCase()} coverage for ${test.requirement}.`,
    };
  });
  if (request.scenario) tests = tests.filter(test => test.category === request.scenario);
  if (!tests.length) return {
    content: `The selected evidence does not support ${request.scenario ? `${request.scenario.toLowerCase()} ` : ''}${request.kind.toLowerCase()} test cases${words.length ? ' for that scope' : ''}. Add a reviewed source-linked requirement${request.kind === 'Technical' ? ' backed by a Technical document' : ''}, or change the requested scope. I have not invented expected results.`,
    citations: [], needsEvidence: true, suggestions: ['Show sources for the selected knowledge'],
  };
  if (previous && request.append && previous.kind === request.kind) {
    const existing = new Map(previous.tests.map(test => [`${test.requirement}:${test.category}:${test.expected}`, structuredClone(test)]));
    tests.forEach(test => existing.set(`${test.requirement}:${test.category}:${test.expected}`, test));
    tests = [...existing.values()];
  }
  if (request.count) tests = tests.slice(0, request.count);
  tests.forEach((test, index) => { test.id = `TC-${workspace.team.toUpperCase()}-${String(index + 1).padStart(3, '0')}`; });
  const sourceIds = [...new Set(tests.flatMap(test => test.sourceIds))];
  const citations = sourceIds.map(id => sourceCitation(docMap.get(id)));
  Object.assign(suite, {
    name: `${workspace.name} ยท ${request.kind} tests`, kind: request.kind, tests,
    sourceSnapshot: citations, origin: 'chat', requestPrompt: request.prompt,
    requestScope: { words, crRefs }, contextSnapshot: request.context,
  });
  return {
    content: `${previous ? 'Updated' : 'Prepared'} ${tests.length} ${request.kind.toLowerCase()} test case${tests.length === 1 ? '' : 's'} from the selected requirements.${request.count && tests.length < request.count ? ` The evidence supports ${tests.length} cases; I have not filled the requested count with unsupported cases.` : ''} Review the draft below, refine it in chat, or save it to Test suites.`,
    citations, draft: suite, suggestions: ['Add negative scenarios', 'Show sources for these test cases', 'Focus only on member identifier validation'],
  };
}

// Backend integration boundary: supply a transport that returns this same response shape.
// The current adapter performs local evidence lookup; it never calls an LLM or exposes API keys.
export async function requestWorkspaceAssistant(request, { state, signal, transport } = {}) {
  if (signal?.aborted) throw new DOMException('Request stopped.', 'AbortError');
  const workspace = assertChatContext(state, request);
  const response = transport ? await transport(request, { signal }) : ['generate', 'refine'].includes(request.intent)
    ? generateCases(state, workspace, request) : answerQuestion(state, workspace, request);
  if (signal?.aborted) throw new DOMException('Request stopped.', 'AbortError');
  const allowed = new Set(request.context.sources.filter(source => ['Ready', 'Reviewed locally'].includes(source.status)).map(source => source.id));
  const docs = new Map(eligibleDocuments(state, workspace).filter(doc => allowed.has(doc.id)).map(doc => [doc.id, doc]));
  if (!response || typeof response.content !== 'string' || !Array.isArray(response.citations)
    || response.citations.some(citation => !citation || !allowed.has(citation.id) || String(citation.revision) !== String(docs.get(citation.id)?.revision)
      || (citation.checksum || '') !== (docs.get(citation.id)?.checksum || '') || (citation.quote != null && typeof citation.quote !== 'string'))) throw new Error('The assistant returned an invalid evidence reference. Try again.');
  response.citations = response.citations.map(citation => sourceCitation(docs.get(citation.id), citation.quote));
  const cited = new Set(response.citations.map(citation => citation.id));
  const template = state.templates.find(item => item.id === request.templateId && item.team === workspace.team && (item.visibility !== 'workspace' || item.workspaceId === workspace.id));
  if (response.draft && (!Array.isArray(response.draft.tests) || !response.draft.tests.length
    || response.draft.workspaceId !== workspace.id || response.draft.version !== workspace.version
    || !['Technical', 'Functional', 'Business'].includes(response.draft.kind)
    || !template || response.draft.templateId !== template.id || !response.draft.templateSnapshot || JSON.stringify(response.draft.templateSnapshot.fields) !== JSON.stringify(template.fields)
    || response.draft.tests.some(test => !Array.isArray(test.sourceIds) || !test.sourceIds.length || test.sourceIds.some(id => !cited.has(id))
      || ['id', 'title', 'expected', 'objective', 'preconditions', 'data', 'category', 'priority', 'requirement'].some(field => typeof test[field] !== 'string')
      || !test.expected.trim() || !Array.isArray(test.steps) || !test.steps.length || test.steps.some(step => typeof step !== 'string')
      || (test.customFields && Object.values(test.customFields).some(value => typeof value !== 'string'))))) {
    throw new Error('The assistant returned an invalid test-case draft. Try again.');
  }
  if (response.draft) {
    response.draft.templateSnapshot = structuredClone(template);
    response.draft.sourceSnapshot = structuredClone(response.citations);
    response.draft.contextSnapshot = structuredClone(request.context);
  }
  if (response.suggestions && (!Array.isArray(response.suggestions) || response.suggestions.some(item => typeof item !== 'string'))) throw new Error('The assistant returned invalid follow-up suggestions. Try again.');
  return { ...response, intent: request.intent, requestContext: request.context, engine: transport ? 'connected' : 'local-evidence' };
}

export function saveChatDraft(state, workspace, messageId, threadKey = contextKey(workspace)) {
  if (threadKey !== contextKey(workspace)) throw new Error('Historical conversations are read-only.');
  const message = state.threads[threadKey]?.messages.find(item => item.id === messageId);
  if (!message?.draft?.tests?.length) throw new Error('Generate a test-case draft first.');
  assertChatContext(state, { context: message.requestContext });
  if (message.savedSuiteId) {
    if (state.suites.some(suite => suite.id === message.savedSuiteId)) return message.savedSuiteId;
    throw new Error('The saved suite is no longer available.');
  }
  const suite = structuredClone(message.draft);
  suite.id = uid('suite');
  suite.createdAt = timestamp(); suite.chatMessageId = message.id;
  state.suites.unshift(suite); message.savedSuiteId = suite.id;
  return suite.id;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)