From 728d56bb5b13e1254dd78a95b52200d53f1b41b6 Mon Sep 17 00:00:00 2001 From: Marcelo Arbore Date: Wed, 29 Apr 2026 03:25:57 -0300 Subject: [PATCH] README + add tests cases --- README.md | 80 +++++ SKILL.md | 16 + references/script-catalog.md | 14 + scripts/fetch-sales-intelligence.mjs | 455 ++++++++++++++++++++++++++- tests/README.md | 9 +- tests/functions.test.mjs | 154 ++++++++- tests/prompts/sales-prompts.json | 35 +++ tests/sales-prompts.test.mjs | 122 +++++++ 8 files changed, 876 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e69de29..4f904a7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,80 @@ +# Knowledge One View CLI Skill + +This Codex skill lets Codex fetch and analyze Oracle Analytics LAD Knowledge One View data through direct Oracle DV backend APIs, without manual browser scraping. + +Use it for sales, presales, pipeline, opportunity, workload, revenue-line, service-request, and team-result questions. Codex can also turn the data into follow-up work: targeted email drafts, spreadsheets, presentations, WON mapping, resource summaries, and manager-ready outputs. + +## What Codex Can Do With This Skill + +With this skill installed, Codex can help you: + +- Fetch opportunity, pipeline, workload, revenue-line, SR, and resource-involvement data. +- Filter by close date, month, fiscal quarter, week, custom window, seller, manager, resource, or team. +- Map WON activity for a team, seller, manager, resource, or fiscal week. +- Generate spreadsheets, presentation-ready summaries, and targeted email drafts from the results. +- Return answers with scope, source, defaults, filters, row counts, warnings, and assumptions. + +In practice, you can ask Codex questions like: + +```text +Show Brazil opportunities closing in FY26-Q4 for seller . +``` + +```text +List workloads closing next week that have allocated resources under manager . +``` + +```text +Get SRs for resource and explain the scope used. +``` + +```text +Map WON workloads for team in week and generate a spreadsheet. +``` + +```text +Draft email follow-ups for resources in based on the WON validation results. +``` + +Codex will choose the shortest safe path, normally by running `scripts/fetch-sales-intelligence.mjs` with an intent, view, query, or SQL mode. + +## How It Works + +Install the skill, then ask Codex for Knowledge One View data or an output based on it. If authentication is needed, Codex opens Chrome so you can complete the Oracle login flow. + +Chrome is mandatory for authentication, especially for SSO or MFA. After the session is valid, Codex continues browserless: it calls the Oracle DV backend APIs directly, receives the backend data, applies the requested filters or intent, and returns the answer or creates the requested output. Chrome is reopened only when the saved session expires or needs to be refreshed. + +## Installation From Git + +Clone the skill: + +```bash +cd ~/.codex/skills +git clone https://git.tech-lad.com.br/marceloarbore/knowledge-one-view-cli.git +``` + +Create a branch before editing: + +```bash +cd ~/.codex/skills/knowledge-one-view-cli +git checkout -b feature/my-change +``` + +If it already exists, update it first: + +```bash +cd ~/.codex/skills/knowledge-one-view-cli +git pull origin main +git checkout -b feature/my-change +``` + +After installing or updating the skill, restart Codex or reload the session so the skill list is refreshed. + +Live data access requires permission to access the Sales Intelligence Knowledge One View workbook at `salesintelligence-dv.oracle.com`. + +## Notes For Contributors + +- Keep stable Codex instructions in `SKILL.md`. +- Put detailed CLI behavior and field mappings in `references/`. +- Add tests when changing intents, query behavior, SQL builders, scope disclosure, or output contracts. +- Keep generated outputs, temporary files, and local workspace artifacts outside the skill repository whenever possible. diff --git a/SKILL.md b/SKILL.md index 634617b..5dca83a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -78,10 +78,20 @@ Use intents first for recurring business asks. Use when the ask names one explicit resource and you need opportunities in a close-date window tied to that person through coverage data, with commercial fields coming from Knowledge DV - Opportunities - V2. - `opportunities-by-manager-close-window` Use when the ask names one explicit manager email and you need opportunities in a close-date window tied to that hierarchy anchor, with commercial fields coming from Knowledge DV - Opportunities - V2. +- `opportunities-by-owner-close-window` + Use when the ask names one explicit territory owner or seller and you need opportunities in a close-date window. +- `opportunities-by-owner-manager-close-window` + Use when the ask names a manager of territory owners and you need opportunities in a close-date window. - `workloads-by-owner-current` Use for current-state workload asks for one explicit territory owner. It defaults to `current-quarter`, `Cloud Infrastructure - Workloads`, and `WORKLOAD`. - `workloads-by-owner-manager-current` Use for current-state workload asks for owners under one explicit manager-of-owner. It defaults to `current-quarter`, `Cloud Infrastructure - Workloads`, and `WORKLOAD`. +- `workloads-by-owner-close-window` + Use when the ask names one explicit territory owner or seller and asks for workloads in an explicit close-date window. +- `workloads-by-owner-manager-close-window` + Use when the ask names a manager of territory owners and asks for workloads in an explicit close-date window. +- `workloads-by-resource-close-window` + Use when the ask names one explicit allocated resource and asks for workload revenue lines in a close-date window. - `workloads-by-manager-resource-close-window` Use for org-wide workload revenue lines closing inside a direct date window when the ask is about opportunities that have allocated resources under one or more named managers. This intent gets manager/resource linkage only from `Knowledge DV - Presales Involvement Aux - V1`, gets commercial rows only from `Knowledge DV - Opportunities - V2`, filters on `Revenue Line Close Date`, and does not query `DV - SE Team` unless the user explicitly asks for SR details. - `srs-by-resource` @@ -140,6 +150,12 @@ Use `--sql` only when `--query` cannot express the ask cleanly. - For `opportunities-close-window`, close-window intents, and `sa-attach-*` commercial views, date-window flags now constrain `Revenue Line Close Date` directly, including `--week`, `--quarter`, and explicit `--from-date/--to-date`. - For present-state workload asks about one seller or one seller-manager, prefer `workloads-by-owner-current` or `workloads-by-owner-manager-current`. - For current-team coverage asks, prefer `opportunities-current-team-close-window`. +- Treat `país` or `country` wording as `--cluster` only when the user names a supported commercial cluster. Do not invent a separate `--country` flag. +- For seller or territory-owner opportunity asks with explicit dates, prefer `opportunities-by-owner-close-window`. +- For seller-manager opportunity asks with explicit dates, prefer `opportunities-by-owner-manager-close-window`. +- For resource workload asks with explicit dates, prefer `workloads-by-resource-close-window`. +- For territory-owner workload asks with explicit dates, prefer `workloads-by-owner-close-window`; keep `workloads-by-owner-current` for current-state or current-quarter asks. +- For territory-owner-manager workload asks with explicit dates, prefer `workloads-by-owner-manager-close-window`; keep `workloads-by-owner-manager-current` for current-state or current-quarter asks. - For asks like `recursos do em pipe na week 2`, prefer `opportunities-by-manager-close-window --manager-email --week w2`. - For asks like `workloads com close date nos proximos 7 dias em oportunidades com recursos abaixo de e `, prefer `workloads-by-manager-resource-close-window --manager-email-list , --from-date today --to-date +7d`. - For that workload-by-manager-resource intent, treat `resourceEmails` as the org-wide allocated resource list from Presales Involvement Aux. The default result is involvement/allocation only; do not fetch SR details unless the user explicitly asks for SRs. diff --git a/references/script-catalog.md b/references/script-catalog.md index 216277f..8b80e19 100644 --- a/references/script-catalog.md +++ b/references/script-catalog.md @@ -371,6 +371,20 @@ node .\scripts\fetch-sales-intelligence.mjs ` --forecast-type Forecast ``` +### Focused Owner And Resource Close-Window Intents + +Use these when the user names one business anchor and an explicit date, week, month, quarter, or relative close-date window. + +| Intent | Anchor | Best use | +|---|---|---| +| `opportunities-by-owner-close-window` | `--owner-email` | Opportunities/workloads for one territory owner. | +| `opportunities-by-owner-manager-close-window` | `--owner-manager-email` | Opportunities/workloads for owners under one territory-owner manager. | +| `workloads-by-resource-close-window` | `--resource` | Workload revenue lines where the named resource appears in Presales Involvement Aux. | +| `workloads-by-owner-close-window` | `--owner-email` | Workload revenue lines for one territory owner. | +| `workloads-by-owner-manager-close-window` | `--owner-manager-email` | Workload revenue lines for owners under one territory-owner manager. | + +All five support `--date`, `--month`, `--quarter`, `--week`, `--week-year`, `--from-date`, `--to-date`, `--cluster`, and `--limit`. Workload intents also support `--forecast-type`; opportunity intents support `--opty-status`. Use `--cluster` as the available geography filter; do not add or use a separate `--country` flag. + ### SA Attach flags Used by `sa-attach-*` views except `sa-attach-hours-by-opportunity` and `sa-attach-presales-coverage`. diff --git a/scripts/fetch-sales-intelligence.mjs b/scripts/fetch-sales-intelligence.mjs index 2da4250..11e71c5 100644 --- a/scripts/fetch-sales-intelligence.mjs +++ b/scripts/fetch-sales-intelligence.mjs @@ -1530,11 +1530,18 @@ const VIEW_DEFS = { }, ], fields: OPPORTUNITIES_CLOSE_WINDOW_COLUMNS.map((column) => column.name), - buildSql: ({ fromDateSql, toDateSql, statuses, cluster = null }) => + buildSql: ({ fromDateSql, toDateSql, statuses, cluster = null, ownerEmail = null, ownerManagerEmail = null }) => buildSelectQuery({ columns: PRIMARY_OPPORTUNITY_WINDOW_COLUMNS, fromClause: `FROM ${SA_ATTACH_OPPORTUNITIES_DATASET}`, - whereClauses: buildPrimaryOpportunityWhereClauses({ fromDateSql, toDateSql, statuses, cluster }), + whereClauses: buildPrimaryOpportunityWhereClauses({ + fromDateSql, + toDateSql, + statuses, + cluster, + ownerEmail, + ownerManagerEmail, + }), groupBy: [ xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Opportunity ID"), xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Opportunity Name"), @@ -1687,6 +1694,66 @@ function workloadClusterParameter() { }; } +function closeWindowDateParameters() { + return [ + { + name: "fromDate", + cli: "--from-date", + required: false, + default: "today", + description: "Start of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.", + }, + { + name: "toDate", + cli: "--to-date", + required: false, + default: "+7d", + description: "End of the close-date window. Accepts YYYY-MM-DD, today, +7d, -3d, or today+7d.", + }, + { + name: "date", + cli: "--date", + required: false, + description: "Exact close date. Shortcut for a one-day window.", + }, + { + name: "month", + cli: "--month", + required: false, + description: "Calendar month in YYYY-MM format. Shortcut for a full-month window.", + }, + { + name: "quarter", + cli: "--quarter", + required: false, + description: "Fiscal quarter in FY26-Q4 or Q4 FY26 format. Shortcut for a full fiscal-quarter window.", + }, + { + name: "week", + cli: "--week", + required: false, + description: "Annual week shortcut anchored on June 1. Accepts 1..53, w1..w53, or week 1..week 53.", + }, + { + name: "weekYear", + cli: "--week-year", + required: false, + default: "current June-cycle start year", + description: "Cycle start year used with --week. For example, 2025 means the 2025-06-01 through 2026-05-31 cycle.", + }, + ]; +} + +function limitParameter(description = "Optional row limit for the extract.") { + return { + name: "limit", + cli: "--limit", + required: false, + default: "5000", + description, + }; +} + const INTENT_CATALOG = { "opportunities-default": { description: "Default opportunity path using the traced commercial pipeline defaults on Knowledge DV - Opportunities - V2.", @@ -1781,6 +1848,81 @@ const INTENT_CATALOG = { `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent workloads-by-owner-manager-current --owner-manager-email territory.manager@oracle.com --forecast-type Forecast`, ], }, + "workloads-by-owner-close-window": { + description: "List org-wide workload revenue lines in a close-date window for one explicit territory owner.", + view: "sa-attach-pipeline-details", + parameters: [ + { + name: "ownerEmail", + cli: "--owner-email", + required: true, + description: "Territory-owner email to match on the direct owner of the workload row.", + }, + ...closeWindowDateParameters(), + { + name: "forecastType", + cli: "--forecast-type", + required: false, + default: "all forecast types", + description: "Optional forecast-type filter such as Forecast, Upside, or Won.", + }, + workloadClusterParameter(), + limitParameter("Optional row limit for the workload extract."), + ], + examples: [ + `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent workloads-by-owner-close-window --owner-email territory.owner@oracle.com --month 2026-05`, + ], + }, + "workloads-by-owner-manager-close-window": { + description: "List org-wide workload revenue lines in a close-date window for territory owners under one explicit owner manager.", + view: "sa-attach-pipeline-details", + parameters: [ + { + name: "ownerManagerEmail", + cli: "--owner-manager-email", + required: true, + description: "Upper territory-owner manager email to match in the workload owner hierarchy.", + }, + ...closeWindowDateParameters(), + { + name: "forecastType", + cli: "--forecast-type", + required: false, + default: "all forecast types", + description: "Optional forecast-type filter such as Forecast, Upside, or Won.", + }, + workloadClusterParameter(), + limitParameter("Optional row limit for the workload extract."), + ], + examples: [ + `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent workloads-by-owner-manager-close-window --owner-manager-email territory.manager@oracle.com --week w43`, + ], + }, + "workloads-by-resource-close-window": { + description: "List org-wide workload revenue lines in a close-date window for opportunities where one explicit resource appears in Presales Involvement Aux coverage.", + view: "sa-attach-pipeline-details", + parameters: [ + { + name: "resource", + cli: "--resource", + required: true, + description: "Oracle user identifier. Accepts resource.member or resource.member@oracle.com.", + }, + ...closeWindowDateParameters(), + { + name: "forecastType", + cli: "--forecast-type", + required: false, + default: "all forecast types", + description: "Optional forecast-type filter such as Forecast, Upside, or Won.", + }, + workloadClusterParameter(), + limitParameter("Optional row limit for the org-wide workload revenue-line extract before coverage intersection."), + ], + examples: [ + `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent workloads-by-resource-close-window --resource resource.member --week w43`, + ], + }, "workloads-by-manager-resource-close-window": { description: "List org-wide workload revenue lines closing inside a date window, keeping only opportunities that have allocated presales resources under one or more explicit managers from Knowledge DV - Presales Involvement Aux - V1. This default intent is involvement/allocation only and does not fetch SR details from DV - SE Team.", @@ -1929,6 +2071,56 @@ const INTENT_CATALOG = { `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent opportunities-close-quarter --quarter current-quarter`, ], }, + "opportunities-by-owner-close-window": { + description: "List org-wide commercial opportunities/workloads in a close-date window for one explicit territory owner.", + view: "opportunities-close-window", + parameters: [ + { + name: "ownerEmail", + cli: "--owner-email", + required: true, + description: "Territory-owner email to match on the direct owner of the opportunity row.", + }, + ...closeWindowDateParameters(), + { + name: "optyStatus", + cli: "--opty-status", + required: false, + default: "all statuses", + description: "Optional comma-separated opportunity statuses such as Open,Won,Closed.", + }, + closeWindowClusterParameter(), + limitParameter("Optional row limit for the opportunity window."), + ], + examples: [ + `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent opportunities-by-owner-close-window --owner-email territory.owner@oracle.com --week w43`, + ], + }, + "opportunities-by-owner-manager-close-window": { + description: "List org-wide commercial opportunities/workloads in a close-date window for territory owners under one explicit owner manager.", + view: "opportunities-close-window", + parameters: [ + { + name: "ownerManagerEmail", + cli: "--owner-manager-email", + required: true, + description: "Upper territory-owner manager email to match in the opportunity owner hierarchy.", + }, + ...closeWindowDateParameters(), + { + name: "optyStatus", + cli: "--opty-status", + required: false, + default: "all statuses", + description: "Optional comma-separated opportunity statuses such as Open,Won,Closed.", + }, + closeWindowClusterParameter(), + limitParameter("Optional row limit for the opportunity window."), + ], + examples: [ + `node "${path.resolve(process.argv[1] || "fetch-sales-intelligence.mjs")}" --intent opportunities-by-owner-manager-close-window --owner-manager-email territory.manager@oracle.com --month 2026-05`, + ], + }, "opportunities-current-team-close-window": { description: "List commercial opportunities/workloads in a close-date window and enrich them with current-team presales/SR coverage for the executing user when present. Org-wide opportunity rows are preserved even when no current-team resource matches.", @@ -4203,6 +4395,15 @@ function applyCurrentWorkloadIntentDefaults(args) { return nextArgs; } +function applyCloseWindowIntentDefaults(args) { + const nextArgs = { ...args }; + if (!hasAnyDateWindowArgs(nextArgs)) { + nextArgs["from-date"] = "today"; + nextArgs["to-date"] = "+7d"; + } + return nextArgs; +} + function resolveSaAttachFilters(args, definition) { const statuses = splitCsv(args["revenue-line-status"], definition.defaultStatuses || SA_ATTACH_DEFAULT_ALL_STATUSES); const cluster = String(args.cluster || SA_ATTACH_DEFAULT_CLUSTER).trim(); @@ -4416,7 +4617,14 @@ function resolveCoverageLimit(limit) { return Math.min(10000, Math.max(5000, Number(limit) * 8)); } -function buildManagerResourceWorkloadSql({ fromDateSql, toDateSql, cluster = SA_ATTACH_DEFAULT_CLUSTER }) { +function buildWorkloadCloseWindowSql({ + fromDateSql, + toDateSql, + cluster = SA_ATTACH_DEFAULT_CLUSTER, + ownerEmail = null, + ownerManagerEmail = null, + forecastTypes = [], +}) { return buildDenseQuery({ columns: WORKLOAD_MANAGER_RESOURCE_PIPELINE_COLUMNS, fromClause: `FROM ${SA_ATTACH_OPPORTUNITIES_DATASET}`, @@ -4433,7 +4641,9 @@ function buildManagerResourceWorkloadSql({ fromDateSql, toDateSql, cluster = SA_ revenueTypeGroups: ["WORKLOAD"], salesCreditType: SA_ATTACH_DEFAULT_SALES_CREDIT_TYPE, minOpportunityProbability: SA_ATTACH_DEFAULT_MIN_OPPORTUNITY_PROBABILITY, - forecastTypes: [], + ownerEmail, + ownerManagerEmail, + forecastTypes, }), orderBy: `${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Revenue Line Close Date")}, ${xsaColumn( SA_ATTACH_OPPORTUNITIES_DATASET, @@ -4442,6 +4652,10 @@ function buildManagerResourceWorkloadSql({ fromDateSql, toDateSql, cluster = SA_ }); } +function buildManagerResourceWorkloadSql({ fromDateSql, toDateSql, cluster = SA_ATTACH_DEFAULT_CLUSTER }) { + return buildWorkloadCloseWindowSql({ fromDateSql, toDateSql, cluster }); +} + function buildRevenueTeamFilter() { return buildXsaTeamFilter(SA_ATTACH_REVENUE_LINES_DATASET); } @@ -4528,6 +4742,8 @@ function buildPrimaryOpportunityWhereClauses({ toDateSql = null, fiscalYear = null, cluster = null, + ownerEmail = null, + ownerManagerEmail = null, }) { const clauses = []; const closeDateExpr = xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Revenue Line Close Date"); @@ -4551,6 +4767,20 @@ function buildPrimaryOpportunityWhereClauses({ if (statuses.length > 0) { clauses.push(`${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Opportunity Status")} IN (${toSqlList(statuses)})`); } + const normalizedOwnerEmail = String(ownerEmail || "").trim().toLowerCase(); + if (normalizedOwnerEmail) { + clauses.push(`${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Level 7 Territory Owner E-mail")} = ${quoteLiteral(normalizedOwnerEmail)}`); + } + const normalizedOwnerManagerEmail = String(ownerManagerEmail || "").trim().toLowerCase(); + if (normalizedOwnerManagerEmail) { + clauses.push(`( + ${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Level 2 Territory Owner E-mail")} = ${quoteLiteral(normalizedOwnerManagerEmail)} + OR ${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Level 3 Territory Owner E-mail")} = ${quoteLiteral(normalizedOwnerManagerEmail)} + OR ${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Level 4 Territory Owner E-mail")} = ${quoteLiteral(normalizedOwnerManagerEmail)} + OR ${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Level 5 Territory Owner E-mail")} = ${quoteLiteral(normalizedOwnerManagerEmail)} + OR ${xsaColumn(SA_ATTACH_OPPORTUNITIES_DATASET, "Level 6 Territory Owner E-mail")} = ${quoteLiteral(normalizedOwnerManagerEmail)} + )`); + } return clauses; } @@ -5655,6 +5885,169 @@ async function executeWorkloadManagerResourceIntent({ }; } +async function executeWorkloadResourceIntent({ + args, + intent, + limit, + state, + meta, + baseUrl, + statePath, + metaPath, + useCache, +}) { + const effectiveArgs = applyCloseWindowIntentDefaults(args); + const dateWindow = resolveDateWindow(effectiveArgs, { from: "today", to: "+7d" }); + const resourceFilter = resolveResourceFilter(effectiveArgs); + const cluster = resolveCommercialClusterFilter(effectiveArgs); + const forecastTypes = splitCsv(effectiveArgs["forecast-type"], []); + const workloadSql = buildWorkloadCloseWindowSql({ + fromDateSql: dateWindow.fromDateSql, + toDateSql: dateWindow.toDateSql, + cluster, + forecastTypes, + }); + const coverageLimit = resolveCoverageLimit(limit); + const [workloadResult] = await executeSqlQueries({ + state, + meta, + baseUrl, + statePath, + metaPath, + useCache, + queries: [{ sql: workloadSql, limit }], + }); + + const workloadRecords = mapRows( + WORKLOAD_MANAGER_RESOURCE_PIPELINE_COLUMNS.map((column) => column.name), + workloadResult?.aResults || [] + ); + const opportunityIds = Array.from(new Set(workloadRecords.map((record) => record.opportunityId).filter(Boolean))); + let coverageSql = null; + let coverageResult = null; + let coverageRecords = []; + if (opportunityIds.length > 0) { + coverageSql = buildExplicitCoverageSqlForOpportunityIds(opportunityIds, resourceFilter.resourceEmailSql); + [coverageResult] = await executeSqlQueries({ + state, + meta, + baseUrl, + statePath, + metaPath, + useCache, + queries: [{ sql: coverageSql, limit: coverageLimit }], + }); + coverageRecords = mapRows( + SA_ATTACH_PRESALES_AUX_COLUMNS.map((column) => column.name), + coverageResult?.aResults || [] + ); + } + + const coverageSummary = summarizeOrgWideRevenueLineCoverage(workloadRecords, coverageRecords); + coverageSummary.summary.resourcesRequested = 1; + + return { + generatedAt: new Date().toISOString(), + scope: buildScopeDescriptor({ + scopeMode: SCOPE_ORG_WIDE, + baseScope: SCOPE_ORG_WIDE, + baseLabel: "Opportunities/workloads", + baseSource: SOURCE_OPPORTUNITIES, + enrichmentScope: SCOPE_ORG_WIDE, + enrichmentLabel: "Manager/resource allocation", + enrichmentSource: SOURCE_PRESALES_AUX, + customDisclosureLines: [ + "Opportunities/workloads: org-wide.", + "Manager/resource allocation: org-wide from Knowledge DV - Presales Involvement Aux - V1.", + ], + components: [ + buildScopeComponent({ + name: "commercialRows", + label: "Opportunities/workloads", + source: SOURCE_OPPORTUNITIES, + scope: SCOPE_ORG_WIDE, + }), + buildScopeComponent({ + name: "managerResourceAllocation", + label: "Manager/resource allocation", + source: SOURCE_PRESALES_AUX, + scope: SCOPE_ORG_WIDE, + }), + ], + }), + apiMetrics: buildApiMetrics([ + { label: "workloadRevenueLines", result: workloadResult, source: SOURCE_OPPORTUNITIES }, + ...(coverageResult ? [{ label: "resourceCoverage", result: coverageResult, source: SOURCE_PRESALES_AUX }] : []), + ]), + request: { + mode: "intent", + intent, + view: "sa-attach-pipeline-details", + secondaryView: "sa-attach-presales-coverage", + endpoint: new URL(SQL_ENDPOINT, baseUrl).toString(), + statePath, + metaPath, + scopeMode: SCOPE_ORG_WIDE, + matchStrategy: + "Filter org-wide workload revenue lines by Revenue Line Close Date, then keep only opportunities where the requested resource appears in Presales Involvement Aux coverage.", + resourceInput: resourceFilter.resourceInput, + resourceEmail: resourceFilter.resourceEmail, + cluster, + forecastTypes, + datePreset: dateWindow.preset, + fromDate: dateWindow.fromDate, + fromDateInput: dateWindow.fromDateInput, + toDate: dateWindow.toDate, + toDateInput: dateWindow.toDateInput, + ...(dateWindow.fiscalQuarter + ? { + fiscalYear: dateWindow.fiscalYear, + fiscalQuarter: dateWindow.fiscalQuarter, + } + : {}), + ...(dateWindow.week + ? { + week: dateWindow.week, + weekLabel: dateWindow.weekLabel, + weekYear: dateWindow.weekYear, + } + : {}), + limit, + coverageLimit, + useCache, + dataSource: SOURCE_OPPORTUNITIES, + coverageSource: SOURCE_PRESALES_AUX, + sql: { + workloadRevenueLines: workloadSql, + resourceCoverage: coverageSql, + }, + }, + columns: WORKLOAD_MANAGER_RESOURCE_CLOSE_WINDOW_COLUMNS, + records: coverageSummary.records, + summary: coverageSummary.summary, + warnings: [ + ...buildWarnings({ + requestedLimit: limit, + recordsCount: workloadResult?.aResults?.length || 0, + customSql: false, + }), + ...(coverageResult + ? buildWarnings({ + requestedLimit: coverageLimit, + recordsCount: coverageResult?.aResults?.length || 0, + customSql: false, + }) + : []), + "Workload rows stay org-wide and do not fall back to DV - SE Team.", + "Resource linkage is taken only from Knowledge DV - Presales Involvement Aux - V1.", + ], + raw: { + workloadRevenueLines: workloadResult, + resourceCoverage: coverageResult, + }, + }; +} + async function executeOpportunityCoverageIntent({ args, intent, @@ -6527,6 +6920,55 @@ async function main() { effectiveArgs = applyCurrentWorkloadIntentDefaults(args); } + if (intent === "opportunities-by-owner-close-window") { + const ownerFilter = resolveOwnerEmailFilter( + args, + "owner-email", + "Pass --owner-email when using opportunities-by-owner-close-window." + ); + effectiveArgs = { ...effectiveArgs, ownerEmail: ownerFilter.ownerEmail }; + } + + if (intent === "opportunities-by-owner-manager-close-window") { + const ownerFilter = resolveOwnerEmailFilter( + args, + "owner-manager-email", + "Pass --owner-manager-email when using opportunities-by-owner-manager-close-window." + ); + effectiveArgs = { ...effectiveArgs, ownerManagerEmail: ownerFilter.ownerEmail }; + } + + if (intent === "workloads-by-owner-close-window") { + resolveOwnerEmailFilter(args, "owner-email", "Pass --owner-email when using workloads-by-owner-close-window."); + effectiveArgs = applyCloseWindowIntentDefaults(args); + } + + if (intent === "workloads-by-owner-manager-close-window") { + resolveOwnerEmailFilter( + args, + "owner-manager-email", + "Pass --owner-manager-email when using workloads-by-owner-manager-close-window." + ); + effectiveArgs = applyCloseWindowIntentDefaults(args); + } + + if (intent === "workloads-by-resource-close-window") { + const payload = await executeWorkloadResourceIntent({ + args: effectiveArgs, + intent, + limit, + state, + meta, + baseUrl, + statePath, + metaPath, + useCache, + }); + const json = await emitJson(effectiveArgs, payload, runtimeHome); + console.log(json); + return; + } + if (intent === "workloads-by-manager-resource-close-window") { const payload = await executeWorkloadManagerResourceIntent({ args: effectiveArgs, @@ -6760,6 +7202,8 @@ async function main() { ...dateWindow, ...resourceFilter, ...(explicitCluster ? { cluster: explicitCluster } : {}), + ...(effectiveArgs.ownerEmail ? { ownerEmail: effectiveArgs.ownerEmail } : {}), + ...(effectiveArgs.ownerManagerEmail ? { ownerManagerEmail: effectiveArgs.ownerManagerEmail } : {}), ...(saAttachFilters ? { saAttachFilters } : {}), }); const fallbackSql = allowFallbackForRequest(definition, { intent, view: resolvedView }) @@ -6887,10 +7331,12 @@ async function main() { } export { + applyCloseWindowIntentDefaults, applyCurrentWorkloadIntentDefaults, buildManagerResourceWorkloadSql, buildPrimaryOpportunityWhereClauses, buildSaAttachOpportunityWhereClauses, + buildWorkloadCloseWindowSql, buildApiMetrics, buildIntentScopeDescriptor, buildScopeDescriptor, @@ -6899,6 +7345,7 @@ export { renderViewSql, resolveDateWindow, resolveCoverageLimit, + resolveManagerEmailListFilter, resolveQuarterInput, resolveSaAttachFilters, summarizeOrgWideRevenueLineCoverage, diff --git a/tests/README.md b/tests/README.md index 2ae329d..7756585 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,10 +2,11 @@ This folder keeps the skill tests intentionally small and executable. -The suite covers only two themes: +The suite covers three offline themes: 1. `functions.test.mjs` checks local functions and output contracts. -2. `sales-prompts.test.mjs` checks whether common sales prompts map to quick, documented CLI commands for Oracle DV. +2. `lib-contracts.test.mjs` checks shared CLI, auth, runtime-path, and output helpers. +3. `sales-prompts.test.mjs` checks whether common sales prompts map to quick, documented CLI commands for Oracle DV, and verifies high-risk render-only CLI contracts such as mode conflicts, scope aliases, and query source inference. The tests read `SKILL.md` and `references/*.md` as the source of truth. They should not keep a parallel copy of long parameter lists, aliases, defaults, or week mappings. @@ -25,6 +26,10 @@ node tests/run.mjs Offline mode does not call Oracle DV. Live mode starts with a no-cache session-context read, then runs prompt-sized backend checks. Prompts that should be org-wide commercial reads assert the primary source from `references/api-map.md`, reject fallback to `DV - SE Team`, and use larger live limits when row volume is part of the expected behavior. +Offline coverage should favor stable behavior over exhaustive combinations. The highest-value cases are date-window resolution, manager/resource normalization, scope disclosure, query source selection, output money formatting, API row metrics, and early CLI validation before any backend call. + +The focused-intent capability matrix protects the intended small surface area: owner, owner-manager, and resource close-window asks get first-class intents, while multi-axis combinations and SR country/owner asks stay out of scope until a real use case justifies them. + Every test invocation of `fetch-sales-intelligence.mjs` is forced through `--no-cache`. When `ORACLE_SI_RUNTIME_HOME` is set, the tests copy `auth-state.json` and `session-meta.json` into a temporary runtime first, so live checks do not rewrite the source runtime. Set `ORACLE_SI_TEST_RUNTIME_HOME` when you intentionally want to provide a writable test runtime. diff --git a/tests/functions.test.mjs b/tests/functions.test.mjs index 9b1825a..43ea40e 100644 --- a/tests/functions.test.mjs +++ b/tests/functions.test.mjs @@ -144,6 +144,22 @@ functionTest( assert.equal(quarter.toDate, "2026-05-31"); const weekYear = parseDefaultWeekYear(catalogs.scriptCatalog); + const firstWeek = fetchModule.resolveDateWindow({ + week: "week 1", + "week-year": String(weekYear), + }); + assert.equal(firstWeek.weekLabel, "W1"); + assert.equal(firstWeek.fromDate, `${weekYear}-06-01`); + assert.equal(firstWeek.toDate, `${weekYear}-06-07`); + + const lastWeek = fetchModule.resolveDateWindow({ + week: "w53", + "week-year": String(weekYear), + }); + assert.equal(lastWeek.weekLabel, "W53"); + assert.equal(lastWeek.fromDate, `${weekYear + 1}-05-31`); + assert.equal(lastWeek.toDate, `${weekYear + 1}-05-31`); + const expectedWeek = parseWeekRow(catalogs.scriptCatalog, "W43"); const week = fetchModule.resolveDateWindow({ week: expectedWeek.weekLabel.toLowerCase(), @@ -157,7 +173,15 @@ functionTest( () => fetchModule.resolveDateWindow({ date: "2026-05-02", month: "2026-05" }), /Use either --date/ ); - return `single=${exact.fromDate}; month=${month.fromDate}..${month.toDate}; quarter=${quarter.fromDate}..${quarter.toDate}; week=${week.fromDate}..${week.toDate}; invalid-combo rejected`; + assert.throws( + () => fetchModule.resolveDateWindow({ week: "w54", "week-year": String(weekYear) }), + /Unsupported week token/ + ); + assert.throws( + () => fetchModule.resolveDateWindow({ "from-date": "2026-05-10", "to-date": "2026-05-02" }), + /from-date 2026-05-10 is after to-date 2026-05-02/ + ); + return `single=${exact.fromDate}; month=${month.fromDate}..${month.toDate}; quarter=${quarter.fromDate}..${quarter.toDate}; week=${week.fromDate}..${week.toDate}; first=${firstWeek.fromDate}; last=${lastWeek.fromDate}; invalid-combo/window rejected`; }); functionTest( @@ -229,7 +253,25 @@ functionTest( }).join("\n"); assert.doesNotMatch(withoutCluster, /"Cluster" =/); - return "explicit cluster is applied; absent cluster preserves existing unfiltered geography behavior"; + const ownerClauses = fetchModule.buildPrimaryOpportunityWhereClauses({ + fromDateSql: "DATE '2026-05-01'", + toDateSql: "DATE '2026-05-31'", + statuses: ["Open"], + cluster: "Brazil", + ownerEmail: "seller.one@oracle.com", + }).join("\n"); + assert.match(ownerClauses, /"Level 7 Territory Owner E-mail" = 'seller.one@oracle.com'/); + + const ownerManagerClauses = fetchModule.buildPrimaryOpportunityWhereClauses({ + fromDateSql: "DATE '2026-05-01'", + toDateSql: "DATE '2026-05-31'", + statuses: ["Open"], + ownerManagerEmail: "sales.manager@oracle.com", + }).join("\n"); + assert.match(ownerManagerClauses, /"Level 2 Territory Owner E-mail" = 'sales.manager@oracle.com'/); + assert.match(ownerManagerClauses, /"Level 6 Territory Owner E-mail" = 'sales.manager@oracle.com'/); + + return "explicit cluster is applied; absent cluster preserves existing unfiltered geography behavior; owner filters are applied"; }); functionTest( @@ -255,6 +297,57 @@ functionTest( return "manager/resource workload cluster is explicit when provided and Brazil by default"; }); +functionTest( + "workload-close-window-sql", + "workload close-window SQL supports date, cluster, owner, manager, and forecast filters", + { + entrada: "buildWorkloadCloseWindowSql com owner, manager e forecast", + }, + () => { + const sql = fetchModule.buildWorkloadCloseWindowSql({ + fromDateSql: "DATE '2026-05-01'", + toDateSql: "DATE '2026-05-31'", + cluster: "Mexico", + ownerEmail: "territory.owner@oracle.com", + ownerManagerEmail: "territory.manager@oracle.com", + forecastTypes: ["Forecast"], + }); + + assert.match(sql, /"Cluster" = 'Mexico'/); + assert.match(sql, /"Revenue Line Close Date" >= DATE '2026-05-01'/); + assert.match(sql, /"Level 7 Territory Owner E-mail" = 'territory.owner@oracle.com'/); + assert.match(sql, /"Level 2 Territory Owner E-mail" = 'territory.manager@oracle.com'/); + assert.match(sql, /"Revenue Line Forecast Type Group" IN \('Forecast'\)/); + return "workload SQL includes geography, window, owner hierarchy, and forecast filters"; +}); + +functionTest( + "manager-email-list-normalization", + "manager email list filters normalize short ids, merge flags, and deduplicate", + { + entrada: "resolveManagerEmailListFilter com lista mista + --manager-email duplicado", + }, + () => { + const result = fetchModule.resolveManagerEmailListFilter({ + "manager-email-list": " Resource.Manager.B , resource.manager.a@oracle.com, RESOURCE.MANAGER.B@ORACLE.COM ", + "manager-email": "resource.manager.a", + }); + + assert.deepEqual(result.managerEmails, [ + "resource.manager.a@oracle.com", + "resource.manager.b@oracle.com", + ]); + assert.equal( + result.managerEmailsSqlList, + "'resource.manager.a@oracle.com', 'resource.manager.b@oracle.com'" + ); + assert.throws( + () => fetchModule.resolveManagerEmailListFilter({ "manager-email-list": " , " }), + /Pass --manager-email-list or --manager-email/ + ); + return `managerEmails=${result.managerEmails.join(",")}`; +}); + functionTest( "coverage-limit-cap", "coverage enrichment limit never exceeds Oracle DV maximum", @@ -297,7 +390,24 @@ functionTest( assert.equal(explicitMonth.month, "2026-04"); assert.equal(explicitMonth.quarter, undefined); assert.equal(explicitMonth.solution, expectedDefaults.solution); - return `quarter=${defaults.quarter}; solution=${defaults.solution}; revenueTypeGroup=${defaults["revenue-type-group"]}; explicitMonth preserved`; + + const ownerWindowDefaults = fetchModule.applyCloseWindowIntentDefaults({ + intent: "workloads-by-owner-close-window", + "owner-email": "territory.owner@oracle.com", + }); + assert.equal(ownerWindowDefaults["from-date"], "today"); + assert.equal(ownerWindowDefaults["to-date"], "+7d"); + assert.equal(ownerWindowDefaults.quarter, undefined); + + const explicitWeek = fetchModule.applyCloseWindowIntentDefaults({ + intent: "workloads-by-owner-close-window", + "owner-email": "territory.owner@oracle.com", + week: "w43", + }); + assert.equal(explicitWeek.week, "w43"); + assert.equal(explicitWeek["from-date"], undefined); + assert.equal(explicitWeek["to-date"], undefined); + return `quarter=${defaults.quarter}; solution=${defaults.solution}; revenueTypeGroup=${defaults["revenue-type-group"]}; explicitMonth preserved; closeWindowDefaults=${ownerWindowDefaults["from-date"]}..${ownerWindowDefaults["to-date"]}`; }); functionTest( @@ -373,6 +483,44 @@ functionTest( return "org-wide sem SEC_ARIA; my-team com SEC_ARIA; SR team usa levels 1..4; lead só quando explícito"; }); +functionTest( + "scope-components-contract", + "scope descriptors expose component-level source and scope details", + { + entrada: "buildIntentScopeDescriptor para mixed current-team e workload manager/resource", + }, + () => { + const mixed = fetchModule.buildIntentScopeDescriptor( + "opportunities-current-team-close-window", + { view: "opportunities-close-window" } + ); + assert.equal(mixed.scopeMode, "mixed-consistent"); + assert.equal(mixed.preserveUnmatchedRows, true); + assert.deepEqual( + mixed.components.map((component) => [component.name, component.scope]), + [ + ["commercialRows", "org-wide"], + ["resourceCoverage", "my-team-only"], + ] + ); + assert.match(mixed.explanation, /Rows without a resource match stay/); + + const orgWide = fetchModule.buildIntentScopeDescriptor( + "workloads-by-manager-resource-close-window", + { view: "opportunities-close-window" } + ); + assert.equal(orgWide.scopeMode, "org-wide"); + assert.deepEqual( + orgWide.components.map((component) => [component.name, component.scope]), + [ + ["commercialRows", "org-wide"], + ["managerResourceAllocation", "org-wide"], + ] + ); + assert.match(orgWide.explanation, /Manager\/resource allocation: org-wide/); + return `mixed=${mixed.components.length}; orgWide=${orgWide.components.length}`; +}); + functionTest( "current-team-summary", "current-team coverage summaries preserve unmatched commercial rows", diff --git a/tests/prompts/sales-prompts.json b/tests/prompts/sales-prompts.json index d2a3a53..147b5cd 100644 --- a/tests/prompts/sales-prompts.json +++ b/tests/prompts/sales-prompts.json @@ -83,6 +83,41 @@ "catalogRefs": ["SKILL.md"], "args": ["--intent", "opportunities-current-team-close-window", "--from-date", "today", "--to-date", "+4d", "--limit", "10"] }, + { + "id": "owner-opportunities-week", + "prompt": "Quais oportunidades do vendedor {{territoryOwnerEmail}} fecham na week 43?", + "catalogRefs": ["SKILL.md", "references/script-catalog.md#week-reference"], + "args": ["--intent", "opportunities-by-owner-close-window", "--owner-email", "{{territoryOwnerEmail}}", "--week", "w43", "--week-year", "catalog-default", "--limit", "10"], + "live": false + }, + { + "id": "owner-manager-opportunities-month", + "prompt": "Quais oportunidades dos owners abaixo de {{territoryManagerEmail}} fecham em maio de 2026?", + "catalogRefs": ["SKILL.md", "references/initial-context-catalog.md"], + "args": ["--intent", "opportunities-by-owner-manager-close-window", "--owner-manager-email", "{{territoryManagerEmail}}", "--month", "2026-05", "--limit", "10"], + "live": false + }, + { + "id": "resource-workloads-week", + "prompt": "Quais workloads do recurso {{resourceMember}} fecham na week 43?", + "catalogRefs": ["SKILL.md", "references/script-catalog.md#week-reference"], + "args": ["--intent", "workloads-by-resource-close-window", "--resource", "{{resourceMember}}", "--week", "w43", "--week-year", "catalog-default", "--limit", "10"], + "live": false + }, + { + "id": "owner-workloads-month", + "prompt": "Quais workloads do vendedor {{territoryOwnerEmail}} fecham em maio de 2026?", + "catalogRefs": ["SKILL.md"], + "args": ["--intent", "workloads-by-owner-close-window", "--owner-email", "{{territoryOwnerEmail}}", "--month", "2026-05", "--limit", "10"], + "live": false + }, + { + "id": "owner-manager-workloads-week", + "prompt": "Quais workloads dos owners abaixo de {{territoryManagerEmail}} fecham na week 43?", + "catalogRefs": ["SKILL.md", "references/initial-context-catalog.md", "references/script-catalog.md#week-reference"], + "args": ["--intent", "workloads-by-owner-manager-close-window", "--owner-manager-email", "{{territoryManagerEmail}}", "--week", "w43", "--week-year", "catalog-default", "--limit", "10"], + "live": false + }, { "id": "ad-hoc-query", "prompt": "Quero opportunityId, cliente, close date, booking e workload das oportunidades de 2026-05-02.", diff --git a/tests/sales-prompts.test.mjs b/tests/sales-prompts.test.mjs index b70785c..300467b 100644 --- a/tests/sales-prompts.test.mjs +++ b/tests/sales-prompts.test.mjs @@ -67,6 +67,29 @@ function globalParameterFlags(parameters) { return new Set((parameters || []).map((parameter) => parameter.cli)); } +const expectedFocusedIntentCapabilities = { + "opportunities-by-owner-close-window": { + required: ["--owner-email"], + optional: ["--date", "--month", "--quarter", "--week", "--week-year", "--from-date", "--to-date", "--cluster", "--opty-status", "--limit"], + }, + "opportunities-by-owner-manager-close-window": { + required: ["--owner-manager-email"], + optional: ["--date", "--month", "--quarter", "--week", "--week-year", "--from-date", "--to-date", "--cluster", "--opty-status", "--limit"], + }, + "workloads-by-resource-close-window": { + required: ["--resource"], + optional: ["--date", "--month", "--quarter", "--week", "--week-year", "--from-date", "--to-date", "--cluster", "--forecast-type", "--limit"], + }, + "workloads-by-owner-close-window": { + required: ["--owner-email"], + optional: ["--date", "--month", "--quarter", "--week", "--week-year", "--from-date", "--to-date", "--cluster", "--forecast-type", "--limit"], + }, + "workloads-by-owner-manager-close-window": { + required: ["--owner-manager-email"], + optional: ["--date", "--month", "--quarter", "--week", "--week-year", "--from-date", "--to-date", "--cluster", "--forecast-type", "--limit"], + }, +}; + test("sales prompt commands are short, direct, and documented in references", () => { const flagsFromReferences = documentedFlags(catalogs); @@ -170,6 +193,32 @@ test("intent prompt commands match the live CLI contract without local flag copi } }); +test("focused owner/resource close-window intents expose the expected offline contract", async () => { + for (const [intentName, expected] of Object.entries(expectedFocusedIntentCapabilities)) { + const result = await runFetch(["--describe-intent", intentName]); + const parameters = result.json.intent.parameters || []; + const flags = new Set(parameters.map((parameter) => parameter.cli)); + const requiredFlags = new Set( + parameters.filter((parameter) => parameter.required).map((parameter) => parameter.cli) + ); + + assert.equal(result.json.intent.name, intentName); + assert.ok(catalogMentionsIntent(catalogs, intentName), `${intentName} must be documented.`); + + for (const flag of expected.required) { + assert.ok(flags.has(flag), `${intentName} is missing ${flag}`); + assert.ok(requiredFlags.has(flag), `${intentName} must require ${flag}`); + } + + for (const flag of expected.optional) { + assert.ok(flags.has(flag), `${intentName} is missing optional ${flag}`); + assert.equal(requiredFlags.has(flag), false, `${intentName} should not require optional ${flag}`); + } + + assert.equal(flags.has("--country"), false, `${intentName} should use --cluster, not a new --country flag.`); + } +}); + test("ad hoc sales prompt renders an org-wide opportunity query", async () => { const item = promptCases.find((entry) => entry.id === "ad-hoc-query"); const args = caseArgs(item); @@ -208,6 +257,79 @@ test("org-wide query money aliases use the pipeline measure that Oracle DV accep assert.match(result.json.expandedSql, /"Columns"\."Revenue Line Close Date" >= DATE '2026-04-26'/); }); +test("query mode keeps commercial auto-scope and SR auto-scope separated", async () => { + const commercial = await runFetch([ + "--query", + "--select", + "opportunityId,customerName", + "--where", + "cluster = 'Brazil'", + "--render-sql", + ]); + assert.equal(commercial.json.query.source, parsePrimaryCommercialSourceName(catalogs.apiMap)); + assert.equal(commercial.json.scope.scopeMode, "org-wide"); + assert.doesNotMatch(commercial.json.expandedSql, /SEC_ARIA_SI-SR-TEAM/); + + const sr = await runFetch([ + "--query", + "--select", + "srNumber,customerName", + "--render-sql", + ]); + assert.equal(sr.json.query.source, "DV - SE Team"); + assert.equal(sr.json.scope.scopeMode, "my-team-only"); + assert.match(sr.json.expandedSql, /SEC_ARIA_SI-SR-TEAM/); +}); + +test("query mode honors my-team scope aliases for otherwise org-wide commercial fields", async () => { + for (const scopeAlias of ["team", "current-team"]) { + const result = await runFetch([ + "--query", + "--select", + "opportunityId,customerName", + "--scope", + scopeAlias, + "--render-sql", + ]); + + assert.equal(result.json.query.requestedScope, "my-team-only", scopeAlias); + assert.equal(result.json.query.effectiveScope, "my-team-only", scopeAlias); + assert.equal(result.json.query.source, "DV - SE Team", scopeAlias); + assert.match(result.json.expandedSql, /SEC_ARIA_SI-SR-TEAM/, scopeAlias); + } +}); + +test("CLI rejects conflicting execution modes and unsupported scopes before backend execution", async () => { + const cases = [ + { + args: ["--query", "--select", "opportunityId", "--sql", "select 1", "--render-sql"], + message: /Use either --query or --sql, not both/, + }, + { + args: ["--query", "--select", "opportunityId", "--intent", "opportunities-default", "--render-sql"], + message: /Use either --query or --intent, not both/, + }, + { + args: ["--query", "--select", "opportunityId", "--view", "opportunity-details", "--render-sql"], + message: /Use either --query or --view, not both/, + }, + { + args: ["--query", "--select", "opportunityId", "--scope", "nope", "--render-sql"], + message: /Unsupported --scope "nope"/, + }, + { + args: ["--render-sql"], + message: /Pass --sql or --query together with --render-sql/, + }, + ]; + + for (const item of cases) { + const result = await runFetch(item.args, { expectCode: 1 }); + assert.match(result.stderr, item.message); + assert.equal(result.stdout, ""); + } +}); + test("CLI rejects invalid limit values before backend execution", async () => { for (const value of ["abc", "0", "-1", "1.5"]) { const result = await runFetch(