192 lines
7.3 KiB
JavaScript
192 lines
7.3 KiB
JavaScript
/**
|
|
* Reproduce "Arc Codex" user's Bug A: the Edit Model dialog's API key
|
|
* field is empty when reopened, even though a key was saved.
|
|
*
|
|
* Steps:
|
|
* 1. Open Models tab.
|
|
* 2. Click "Add Model" — fill in custom provider, base URL, API key, save.
|
|
* 3. Read .env to verify the key was written (and under WHICH env var).
|
|
* 4. Open the same model's Edit dialog.
|
|
* 5. Inspect the API key form field — is it populated, or empty?
|
|
*
|
|
* Expected (if no bug): field shows the saved key (or a placeholder)
|
|
* Symptom: field is empty
|
|
*/
|
|
|
|
const { attach } = require("./e2e-attach");
|
|
const { execFileSync } = require("child_process");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
|
|
const ENV_FILE = path.join(
|
|
os.homedir(),
|
|
"AppData",
|
|
"Local",
|
|
"hermes",
|
|
".env",
|
|
);
|
|
|
|
const TEST_BASE_URL = "https://www.arccodex.com/api/codex/v1";
|
|
const TEST_KEY = "sk-test-arc-codex-AAAA-BBBB-CCCC";
|
|
const TEST_MODEL_NAME = "test-arc-codex-bug-A";
|
|
const TEST_MODEL_ID = "gpt-5.5";
|
|
|
|
function envEntries(file) {
|
|
try {
|
|
const content = require("fs").readFileSync(file, "utf-8");
|
|
const matches = {};
|
|
for (const line of content.split("\n")) {
|
|
const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
|
|
if (m) matches[m[1]] = m[2];
|
|
}
|
|
return matches;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
async function clickByTitle(page, title) {
|
|
// Helper: click a button by its title attribute
|
|
await page.click(`button[title="${title}"]`);
|
|
}
|
|
|
|
async function dumpModalState(page) {
|
|
return await page.evaluate(() => {
|
|
const inputs = Array.from(document.querySelectorAll(".models-modal input, .models-modal select, .models-modal textarea"));
|
|
return inputs.map((el) => ({
|
|
tag: el.tagName.toLowerCase(),
|
|
type: el.getAttribute("type"),
|
|
id: el.id,
|
|
name: el.getAttribute("name"),
|
|
placeholder: el.getAttribute("placeholder"),
|
|
value: el.value,
|
|
}));
|
|
});
|
|
}
|
|
|
|
(async () => {
|
|
const { browser, page } = await attach();
|
|
|
|
// Snapshot .env before
|
|
const beforeEnv = envEntries(ENV_FILE);
|
|
console.log("[setup] .env keys before:", Object.keys(beforeEnv).filter(k => k.endsWith("_API_KEY") || k === "CUSTOM_API_KEY"));
|
|
|
|
// Navigate to Models tab
|
|
await page.click('text=/^Models$/');
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
console.log("[step 1] On Models tab");
|
|
|
|
// Click "Add Model" button (top right)
|
|
await page.evaluate(() => {
|
|
// The button has the addModel translation key as title or text
|
|
const btns = Array.from(document.querySelectorAll("button"));
|
|
const addBtn = btns.find((b) => /add model/i.test(b.textContent || ""));
|
|
if (addBtn) addBtn.click();
|
|
});
|
|
await page.waitForSelector(".models-modal", { timeout: 5000 });
|
|
console.log("[step 2] Add Model dialog open");
|
|
|
|
// Fill form: select OpenAI Compatible / Local provider, fill name, model, base URL, api key
|
|
// First, the provider dropdown — must be "custom"
|
|
await page.selectOption('select#model-form-provider', 'custom');
|
|
// Wait for any reactive updates
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
// Find the various inputs by their visible structure
|
|
const inputs = await dumpModalState(page);
|
|
console.log("[step 2] Modal inputs:");
|
|
for (const i of inputs) console.log(` ${i.tag}[${i.type || ''}] id=${i.id||''} placeholder=${i.placeholder||''} value=${i.value||''}`);
|
|
|
|
// Fill the form — there are 4 text inputs we need:
|
|
// displayName, modelId, baseUrl, apiKey
|
|
// We'll fill by labels
|
|
await page.evaluate(({ name, model, url, key }) => {
|
|
const all = Array.from(document.querySelectorAll(".models-modal input[type='text'], .models-modal input[type='password']"));
|
|
// models.displayName is the first text input typically
|
|
// models.modelId is second; baseUrl third; apiKey fourth/last
|
|
const setVal = (el, value) => {
|
|
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
|
|
setter.call(el, value);
|
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
};
|
|
if (all[0]) setVal(all[0], name);
|
|
if (all[1]) setVal(all[1], model);
|
|
if (all[2]) setVal(all[2], url);
|
|
if (all[3]) setVal(all[3], key);
|
|
}, { name: TEST_MODEL_NAME, model: TEST_MODEL_ID, url: TEST_BASE_URL, key: TEST_KEY });
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
|
|
console.log("[step 3] Form filled, clicking Add Model");
|
|
// Click the "Add Model" / Update button in the modal
|
|
await page.evaluate(() => {
|
|
const btns = Array.from(document.querySelectorAll(".models-modal button"));
|
|
const submitBtn = btns.find((b) => /add model|update/i.test(b.textContent || ""));
|
|
if (submitBtn) submitBtn.click();
|
|
});
|
|
await page.waitForSelector(".models-modal", { state: "hidden", timeout: 5000 }).catch(() => {});
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
console.log("[step 3] Saved.");
|
|
|
|
// Check .env
|
|
const afterEnv = envEntries(ENV_FILE);
|
|
const newKeys = Object.keys(afterEnv).filter((k) => !(k in beforeEnv));
|
|
const changedKeys = Object.keys(afterEnv).filter((k) => k in beforeEnv && afterEnv[k] !== beforeEnv[k]);
|
|
console.log("[step 4] .env changes:");
|
|
for (const k of newKeys) {
|
|
console.log(` NEW: ${k}=${afterEnv[k].slice(0, 12)}… (matches test key prefix? ${afterEnv[k] === TEST_KEY})`);
|
|
}
|
|
for (const k of changedKeys) {
|
|
console.log(` CHANGED: ${k}`);
|
|
}
|
|
if (newKeys.length === 0 && changedKeys.length === 0) {
|
|
console.log(" (no changes — key was not written?)");
|
|
}
|
|
|
|
// Now find the saved model in the list and click the card itself (entire
|
|
// card is the Edit trigger — Models.tsx line 293)
|
|
console.log("[step 5] Clicking the model card to open Edit dialog...");
|
|
await page.evaluate((name) => {
|
|
const cards = Array.from(document.querySelectorAll(".models-card"));
|
|
for (const card of cards) {
|
|
if ((card.textContent || "").includes(name)) {
|
|
card.click();
|
|
return;
|
|
}
|
|
}
|
|
}, TEST_MODEL_NAME);
|
|
await page.waitForSelector(".models-modal", { timeout: 5000 });
|
|
|
|
const reopenInputs = await dumpModalState(page);
|
|
console.log("[step 6] Edit-dialog inputs after reopen:");
|
|
for (const i of reopenInputs) {
|
|
const v = (i.value || "").slice(0, 25);
|
|
console.log(` ${i.tag}[${i.type || ''}] placeholder=${(i.placeholder||'').slice(0,30)} value=${v}`);
|
|
}
|
|
|
|
// The 4th text/password input is the apiKey field per Models.tsx
|
|
const apiKeyInput = reopenInputs.find((i) => (i.placeholder || "").toLowerCase().includes("sk-") || (i.placeholder || "").toLowerCase().includes("key"));
|
|
console.log();
|
|
if (apiKeyInput) {
|
|
if (apiKeyInput.value && apiKeyInput.value.includes("sk-test")) {
|
|
console.log(`[VERDICT] ✅ API key persisted in the form: ${apiKeyInput.value.slice(0, 20)}…`);
|
|
} else if (!apiKeyInput.value) {
|
|
console.log(`[VERDICT] 🔴 REPRODUCED — API key field is empty after reopen, even though .env has the value.`);
|
|
} else {
|
|
console.log(`[VERDICT] ⚠️ Field has unexpected value: ${apiKeyInput.value}`);
|
|
}
|
|
} else {
|
|
console.log(`[VERDICT] ⚠️ Couldn't identify the API key field`);
|
|
}
|
|
|
|
// Cleanup: close the modal
|
|
await page.evaluate(() => {
|
|
const cancelBtn = Array.from(document.querySelectorAll(".models-modal button")).find((b) => /cancel|close/i.test(b.textContent || ""));
|
|
if (cancelBtn) cancelBtn.click();
|
|
});
|
|
|
|
await browser.close();
|
|
})().catch((e) => {
|
|
console.error("FAILED:", e.stack || e.message || e);
|
|
process.exit(1);
|
|
});
|