Primeiro Commit v0.beta1
This commit is contained in:
190
tests/helpers/catalogs.mjs
Normal file
190
tests/helpers/catalogs.mjs
Normal file
@@ -0,0 +1,190 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { skillHome } from "./cli.mjs";
|
||||
|
||||
export async function readSkillCatalogs() {
|
||||
const [skill, scriptCatalog, initialContext, apiMap] = await Promise.all([
|
||||
fs.readFile(path.join(skillHome, "SKILL.md"), "utf8"),
|
||||
fs.readFile(path.join(skillHome, "references", "script-catalog.md"), "utf8"),
|
||||
fs.readFile(path.join(skillHome, "references", "initial-context-catalog.md"), "utf8"),
|
||||
fs.readFile(path.join(skillHome, "references", "api-map.md"), "utf8"),
|
||||
]);
|
||||
return {
|
||||
skill,
|
||||
scriptCatalog,
|
||||
initialContext,
|
||||
apiMap,
|
||||
allText: [skill, scriptCatalog, initialContext, apiMap].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
export function documentedFlags(catalogs) {
|
||||
const flags = new Set();
|
||||
const regex = /`(--[a-z0-9-]+)(?:\s+[^`]*)?`/gi;
|
||||
let match = regex.exec(catalogs.allText);
|
||||
while (match) {
|
||||
flags.add(match[1]);
|
||||
match = regex.exec(catalogs.allText);
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
export function catalogMentionsIntent(catalogs, intentName) {
|
||||
return new RegExp(`\\b${escapeRegExp(intentName)}\\b`).test(catalogs.skill + "\n" + catalogs.scriptCatalog);
|
||||
}
|
||||
|
||||
export function catalogMentionsView(catalogs, viewName) {
|
||||
return new RegExp(`\\b${escapeRegExp(viewName)}\\b`).test(catalogs.skill + "\n" + catalogs.scriptCatalog);
|
||||
}
|
||||
|
||||
export function commandAppearsInCatalog(catalogs, args) {
|
||||
const normalizedCatalog = normalizeWhitespace(catalogs.skill + "\n" + catalogs.scriptCatalog + "\n" + catalogs.apiMap);
|
||||
const normalizedArgs = normalizeWhitespace(args.join(" "));
|
||||
return normalizedCatalog.includes(normalizedArgs);
|
||||
}
|
||||
|
||||
export function parseDefaultWeekYear(scriptCatalog) {
|
||||
const match = /-\s*`week-year\s+(\d{4})`/i.exec(scriptCatalog);
|
||||
if (!match) {
|
||||
throw new Error("Could not parse default week-year from references/script-catalog.md.");
|
||||
}
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
export function parseWeekRow(scriptCatalog, weekLabel) {
|
||||
const normalizedLabel = String(weekLabel).toUpperCase().replace(/^W?/, "W");
|
||||
const regex = new RegExp(`\\|\\s*${escapeRegExp(normalizedLabel)}\\s*\\|\\s*(\\d{4}-\\d{2}-\\d{2})\\s*\\|\\s*(\\d{4}-\\d{2}-\\d{2})\\s*\\|`, "i");
|
||||
const match = regex.exec(scriptCatalog);
|
||||
if (!match) {
|
||||
throw new Error(`Could not parse ${normalizedLabel} from references/script-catalog.md.`);
|
||||
}
|
||||
return {
|
||||
weekLabel: normalizedLabel,
|
||||
fromDate: match[1],
|
||||
toDate: match[2],
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSaAttachDefaults(initialContext) {
|
||||
const line = findLine(initialContext, "SA Attach defaults:");
|
||||
const rawDefaults = line.slice(line.indexOf(":") + 1).split(";").map((part) => part.trim()).filter(Boolean);
|
||||
const defaults = {};
|
||||
for (const part of rawDefaults) {
|
||||
const match = /`--([^=]+)=([^`]+)`/.exec(part);
|
||||
if (!match) continue;
|
||||
defaults[toCamelKey(match[1])] = splitCatalogCsv(match[2]);
|
||||
}
|
||||
return defaults;
|
||||
}
|
||||
|
||||
export function parseSaAttachAliases(initialContext) {
|
||||
const line = findLine(initialContext, "SA Attach aliases:");
|
||||
const aliases = {};
|
||||
const rawAliases = line.slice(line.indexOf(":") + 1).split(";").map((part) => part.trim()).filter(Boolean);
|
||||
for (const item of rawAliases) {
|
||||
const match = /`([^`]+)`\s*->\s*`([^`]+)`/.exec(item);
|
||||
if (!match) continue;
|
||||
aliases[match[1].toLowerCase()] = splitCatalogCsv(match[2]);
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
export function catalogSaysCurrentWorkloadDefaults(catalogs, intentName) {
|
||||
try {
|
||||
parseCurrentWorkloadDefaults(catalogs, intentName);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCurrentWorkloadDefaults(catalogs, intentName) {
|
||||
const source = catalogs.skill + "\n" + catalogs.scriptCatalog;
|
||||
const pattern = new RegExp(
|
||||
`\\\`${escapeRegExp(intentName)}\\\`[\\s\\S]{0,500}?defaults? to \\\`([^\\\`]+)\\\`, \\\`([^\\\`]+)\\\`, and \\\`([^\\\`]+)\\\``,
|
||||
"i"
|
||||
);
|
||||
const match = pattern.exec(source);
|
||||
if (!match) {
|
||||
throw new Error(`Could not parse documented defaults for ${intentName}.`);
|
||||
}
|
||||
return {
|
||||
quarter: match[1],
|
||||
solution: match[2],
|
||||
revenueTypeGroup: match[3],
|
||||
};
|
||||
}
|
||||
|
||||
export function catalogSaysUseRevenueLineCloseDate(catalogs) {
|
||||
try {
|
||||
parseSaAttachDateWindowField(catalogs);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSaAttachDateWindowField(catalogs) {
|
||||
const match =
|
||||
/date-window flags now constrain `([^`]+)` directly/i.exec(catalogs.skill) ||
|
||||
/date-window flags are applied to `([^`]+)`/i.exec(catalogs.scriptCatalog);
|
||||
if (!match) {
|
||||
throw new Error("Could not parse the SA Attach date-window field from references.");
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function parsePrimaryCommercialSubjectArea(apiMap) {
|
||||
const match = /Primary commercial subject area:[\s\S]*?`([^`]+)`/i.exec(apiMap);
|
||||
if (!match) {
|
||||
throw new Error("Could not parse the primary commercial subject area from references/api-map.md.");
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function parsePrimaryCommercialSourceName(apiMap) {
|
||||
const subjectArea = parsePrimaryCommercialSubjectArea(apiMap);
|
||||
const match = /\.'([^']+)'\)$/i.exec(subjectArea);
|
||||
if (!match) {
|
||||
throw new Error("Could not parse the primary commercial source name from references/api-map.md.");
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function catalogSaysPreserveMixedRows(catalogs) {
|
||||
return /preserve every org-wide opportunity\/workload row/i.test(catalogs.skill);
|
||||
}
|
||||
|
||||
export function catalogSaysApiMetricsAreRawRows(catalogs) {
|
||||
return /apiMetrics\.totalRowsPulledFromApi/i.test(catalogs.skill)
|
||||
&& /Do not add local-cache-only rows/i.test(catalogs.skill);
|
||||
}
|
||||
|
||||
export function splitCatalogCsv(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function findLine(text, marker) {
|
||||
const line = String(text)
|
||||
.split(/\r?\n/)
|
||||
.find((candidate) => candidate.includes(marker));
|
||||
if (!line) {
|
||||
throw new Error(`Could not find "${marker}" in catalog text.`);
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
function toCamelKey(flagName) {
|
||||
return flagName.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
||||
}
|
||||
|
||||
function normalizeWhitespace(value) {
|
||||
return String(value).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
168
tests/helpers/cli.mjs
Normal file
168
tests/helpers/cli.mjs
Normal file
@@ -0,0 +1,168 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const helperDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const testsDir = path.resolve(helperDir, "..");
|
||||
export const skillHome = path.resolve(testsDir, "..");
|
||||
export const fetchScriptPath = path.join(skillHome, "scripts", "fetch-sales-intelligence.mjs");
|
||||
|
||||
let preparedRuntimeHomePromise = null;
|
||||
|
||||
export function testRuntimeHome() {
|
||||
return (
|
||||
process.env.ORACLE_SI_TEST_RUNTIME_HOME ||
|
||||
path.join(os.tmpdir(), "knowledge-one-view-cli-tests", "runtime")
|
||||
);
|
||||
}
|
||||
|
||||
async function prepareRuntimeHome() {
|
||||
if (process.env.ORACLE_SI_TEST_RUNTIME_HOME) {
|
||||
await fs.mkdir(process.env.ORACLE_SI_TEST_RUNTIME_HOME, { recursive: true });
|
||||
return process.env.ORACLE_SI_TEST_RUNTIME_HOME;
|
||||
}
|
||||
|
||||
const runtimeHome = testRuntimeHome();
|
||||
if (!process.env.ORACLE_SI_RUNTIME_HOME) {
|
||||
await fs.mkdir(runtimeHome, { recursive: true });
|
||||
return runtimeHome;
|
||||
}
|
||||
|
||||
if (!preparedRuntimeHomePromise) {
|
||||
preparedRuntimeHomePromise = (async () => {
|
||||
await fs.rm(runtimeHome, { recursive: true, force: true });
|
||||
await fs.mkdir(runtimeHome, { recursive: true });
|
||||
for (const fileName of ["auth-state.json", "session-meta.json"]) {
|
||||
await fs.copyFile(
|
||||
path.join(process.env.ORACLE_SI_RUNTIME_HOME, fileName),
|
||||
path.join(runtimeHome, fileName)
|
||||
);
|
||||
}
|
||||
return runtimeHome;
|
||||
})();
|
||||
}
|
||||
|
||||
return preparedRuntimeHomePromise;
|
||||
}
|
||||
|
||||
export function flagsInArgs(args) {
|
||||
return args.filter((arg) => String(arg).startsWith("--"));
|
||||
}
|
||||
|
||||
export function argValue(args, flagName) {
|
||||
const index = args.indexOf(flagName);
|
||||
return index >= 0 ? args[index + 1] : null;
|
||||
}
|
||||
|
||||
export function executionModes(args) {
|
||||
return ["--intent", "--view", "--query", "--sql"].filter((flagName) => args.includes(flagName));
|
||||
}
|
||||
|
||||
export function withoutFlags(args, flagsToRemove) {
|
||||
const flags = new Set(flagsToRemove);
|
||||
const next = [];
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const token = args[index];
|
||||
if (!flags.has(token)) {
|
||||
next.push(token);
|
||||
continue;
|
||||
}
|
||||
if (token !== "--query" && token !== "--render-sql" && args[index + 1] && !args[index + 1].startsWith("--")) {
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function runFetch(cliArgs, options = {}) {
|
||||
const {
|
||||
expectCode = 0,
|
||||
timeoutMs = 120000,
|
||||
runtimeHome: requestedRuntimeHome = null,
|
||||
} = options;
|
||||
const runtimeHome = requestedRuntimeHome || (await prepareRuntimeHome());
|
||||
const noCacheArgs = cliArgs.includes("--no-cache") ? [...cliArgs] : [...cliArgs, "--no-cache"];
|
||||
const finalArgs = noCacheArgs.includes("--runtime-home")
|
||||
? noCacheArgs
|
||||
: [...noCacheArgs, "--runtime-home", runtimeHome];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [fetchScriptPath, ...finalArgs], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: process.env,
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let timedOut = false;
|
||||
const startedAt = process.hrtime.bigint();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
child.kill();
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timer);
|
||||
const durationMs = Number(process.hrtime.bigint() - startedAt) / 1_000_000;
|
||||
let json = null;
|
||||
try {
|
||||
json = stdout.trim() ? JSON.parse(stdout) : null;
|
||||
} catch {}
|
||||
|
||||
const result = {
|
||||
args: finalArgs,
|
||||
code,
|
||||
stdout,
|
||||
stderr,
|
||||
json,
|
||||
timedOut,
|
||||
durationMs,
|
||||
};
|
||||
|
||||
if (expectCode !== null && code !== expectCode) {
|
||||
reject(
|
||||
new Error(
|
||||
[
|
||||
`Unexpected exit code ${code}; expected ${expectCode}.`,
|
||||
`Command: ${process.execPath} ${fetchScriptPath} ${finalArgs.join(" ")}`,
|
||||
stderr.trim() || stdout.trim(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectCode === 0 && (!json || typeof json !== "object")) {
|
||||
reject(
|
||||
new Error(
|
||||
[
|
||||
"Expected JSON output from fetch-sales-intelligence.mjs.",
|
||||
`Command: ${process.execPath} ${fetchScriptPath} ${finalArgs.join(" ")}`,
|
||||
stderr.trim() || stdout.trim(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
142
tests/helpers/examples.mjs
Normal file
142
tests/helpers/examples.mjs
Normal file
@@ -0,0 +1,142 @@
|
||||
import { runFetch } from "./cli.mjs";
|
||||
|
||||
const EMAIL_REGEX = /^[a-z0-9._%+-]+@oracle\.com$/i;
|
||||
const PLACEHOLDER_REGEX = /\{\{([a-zA-Z0-9]+)\}\}/g;
|
||||
|
||||
export function hasPlaceholders(value) {
|
||||
return PLACEHOLDER_REGEX.test(String(value || ""));
|
||||
}
|
||||
|
||||
export function resolvePlaceholders(value, examples = {}, { allowUnresolved = false } = {}) {
|
||||
return String(value).replace(PLACEHOLDER_REGEX, (match, key) => {
|
||||
if (examples[key]) return examples[key];
|
||||
if (allowUnresolved) return match;
|
||||
throw new Error(`Missing live example value for {{${key}}}.`);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolvePromptCase(item, examples = {}, { allowUnresolved = false } = {}) {
|
||||
return {
|
||||
...item,
|
||||
prompt: resolvePlaceholders(item.prompt, examples, { allowUnresolved }),
|
||||
args: item.args.map((arg) => resolvePlaceholders(arg, examples, { allowUnresolved })),
|
||||
...(item.liveArgs
|
||||
? { liveArgs: item.liveArgs.map((arg) => resolvePlaceholders(arg, examples, { allowUnresolved })) }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildLiveExampleContext(catalogs) {
|
||||
const catalogResourceManagers = emailsFromCatalog(catalogs.initialContext);
|
||||
const [pipelineSample, presalesSample] = await Promise.all([
|
||||
runFetch(["--view", "sa-attach-pipeline-details", "--limit", "500"], { timeoutMs: 180000 }),
|
||||
runFetch(["--view", "sa-attach-presales-coverage", "--limit", "500"], { timeoutMs: 180000 }),
|
||||
]);
|
||||
|
||||
const pipelineRecords = pipelineSample.json.records || [];
|
||||
const presalesRecords = presalesSample.json.records || [];
|
||||
const resourceManagerEmails = uniqueEmails([
|
||||
...catalogResourceManagers,
|
||||
...valuesFromRecords(presalesRecords, ["seTeamLevel4EmailAddress"]),
|
||||
]);
|
||||
const resourceMemberEmails = uniqueEmails(valuesFromRecords(presalesRecords, ["seTeamLevel5EmailAddress"]));
|
||||
const territoryOwnerEmails = uniqueEmails(
|
||||
valuesFromRecords(pipelineRecords, ["level7TerritoryOwnerEmail", "territoryOwnerEmail"])
|
||||
);
|
||||
const territoryManagerEmails = uniqueEmails(
|
||||
valuesFromRecords(pipelineRecords, [
|
||||
"level2TerritoryOwnerEmail",
|
||||
"level3TerritoryOwnerEmail",
|
||||
"level4TerritoryOwnerEmail",
|
||||
"level5TerritoryOwnerEmail",
|
||||
"level6TerritoryOwnerEmail",
|
||||
])
|
||||
);
|
||||
const [resourceManagerEmailA, resourceManagerEmailB] = pickDistinct(
|
||||
resourceManagerEmails,
|
||||
2,
|
||||
"resource manager emails"
|
||||
);
|
||||
|
||||
return {
|
||||
examples: {
|
||||
resourceManagerEmail: pickRandom(resourceManagerEmails, "resource manager email"),
|
||||
resourceManagerEmailA,
|
||||
resourceManagerEmailB,
|
||||
resourceManagerEmailList: `${resourceManagerEmailA},${resourceManagerEmailB}`,
|
||||
territoryOwnerEmail: pickRandom(territoryOwnerEmails, "territory owner email"),
|
||||
territoryManagerEmail: pickRandom(territoryManagerEmails, "territory manager email"),
|
||||
resourceMember: pickRandom(resourceMemberEmails, "resource member email"),
|
||||
},
|
||||
trace: [
|
||||
{
|
||||
script: "fetch-sales-intelligence.mjs",
|
||||
args: pipelineSample.args,
|
||||
result: `ok; records=${pipelineRecords.length}; source=${pipelineSample.json.request?.dataSource || "unknown"}`,
|
||||
profiles: ["territoryOwnerEmail", "territoryManagerEmail"],
|
||||
},
|
||||
{
|
||||
script: "fetch-sales-intelligence.mjs",
|
||||
args: presalesSample.args,
|
||||
result: `ok; records=${presalesRecords.length}; source=${presalesSample.json.request?.dataSource || "unknown"}`,
|
||||
profiles: ["resourceManagerEmail", "resourceManagerEmailA", "resourceManagerEmailB", "resourceMember"],
|
||||
},
|
||||
],
|
||||
poolSizes: {
|
||||
resourceManagerEmails: resourceManagerEmails.length,
|
||||
resourceMemberEmails: resourceMemberEmails.length,
|
||||
territoryOwnerEmails: territoryOwnerEmails.length,
|
||||
territoryManagerEmails: territoryManagerEmails.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function placeholderKeysForItem(item) {
|
||||
const text = [item.prompt, ...(item.args || []), ...(item.liveArgs || [])].join("\n");
|
||||
return Array.from(text.matchAll(PLACEHOLDER_REGEX), (match) => match[1]);
|
||||
}
|
||||
|
||||
function emailsFromCatalog(text) {
|
||||
return uniqueEmails(Array.from(String(text || "").matchAll(/[a-z0-9._%+-]+@oracle\.com/gi), (match) => match[0]));
|
||||
}
|
||||
|
||||
function valuesFromRecords(records, fieldNames) {
|
||||
const values = [];
|
||||
for (const record of records || []) {
|
||||
for (const fieldName of fieldNames) {
|
||||
if (record?.[fieldName]) values.push(record[fieldName]);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function uniqueEmails(values) {
|
||||
return Array.from(
|
||||
new Set(
|
||||
values
|
||||
.map((value) => String(value || "").trim().toLowerCase())
|
||||
.filter((value) => EMAIL_REGEX.test(value))
|
||||
.filter((value) => !/^(resource|territory)\./.test(value))
|
||||
)
|
||||
).sort();
|
||||
}
|
||||
|
||||
function pickRandom(values, label) {
|
||||
if (!values.length) {
|
||||
throw new Error(`Could not resolve any real ${label} from the skill catalogs or live DV sample.`);
|
||||
}
|
||||
return values[Math.floor(Math.random() * values.length)];
|
||||
}
|
||||
|
||||
function pickDistinct(values, count, label) {
|
||||
if (values.length < count) {
|
||||
throw new Error(`Could not resolve ${count} distinct real ${label} from the skill catalogs or live DV sample.`);
|
||||
}
|
||||
const remaining = [...values];
|
||||
const picked = [];
|
||||
while (picked.length < count) {
|
||||
const index = Math.floor(Math.random() * remaining.length);
|
||||
picked.push(remaining.splice(index, 1)[0]);
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
Reference in New Issue
Block a user