HEX
Server: Apache/2.4.46 (Win64) OpenSSL/1.1.1j PHP/8.4.25
System: Windows NT DESKTOP-4TAV2RJ 10.0 build 19045 (Windows 10) AMD64
User: fred (0)
PHP: 8.4.25
Disabled: NONE
Upload Files
File: C:/Users/fred/.codex/.tmp/plugins/plugins/openai-developers/tests/openai-platform-api-key.test.mjs
import { execFileSync, spawnSync } from "node:child_process";
import { webcrypto } from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import assert from "node:assert/strict";
import test from "node:test";
import { fileURLToPath } from "node:url";

const { subtle } = webcrypto;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT = path.resolve(
  __dirname,
  "../scripts/openai-platform-api-key.mjs",
);
const SKILL = path.resolve(
  __dirname,
  "../skills/openai-platform-api-key/SKILL.md",
);
const EVALS = path.resolve(
  __dirname,
  "../skills/openai-platform-api-key/references/evals.md",
);
const OPENAI_DOCS_SKILL = path.resolve(
  __dirname,
  "../../../skills/skills/openai-docs/SKILL.md",
);
const APPLIED_OPENAI_DOCS_SKILL = path.resolve(
  __dirname,
  "../../../lib/applied/applied_skills/applied_skills/example_skills/openai-docs/current/SKILL.md",
);
const PLUGIN_ICON = path.resolve(
  __dirname,
  "../assets/openai-platform.png",
);
const APP_ICON = path.resolve(
  __dirname,
  "../../../chatgpt/web/public/images/ecosystem/apps/openai_platform/icon.png",
);
const SECRET = "sk-proj-test-secret-value";

function runScript(args) {
  return execFileSync(process.execPath, [SCRIPT, ...args], {
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"],
  });
}

function runScriptFailure(args) {
  return spawnSync(process.execPath, [SCRIPT, ...args], {
    encoding: "utf8",
    stdio: ["ignore", "pipe", "pipe"],
  });
}

function base64url(bytes) {
  return Buffer.from(bytes).toString("base64url");
}

async function encryptWithPublicJwk(publicJwk, plaintext) {
  const publicKey = await subtle.importKey(
    "jwk",
    publicJwk,
    {
      name: "RSA-OAEP",
      hash: "SHA-256",
    },
    false,
    ["encrypt"],
  );
  const ciphertext = await subtle.encrypt(
    {
      name: "RSA-OAEP",
    },
    publicKey,
    new TextEncoder().encode(plaintext),
  );
  return base64url(ciphertext);
}

test("prepare writes private key locally and emits a public connector request", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--name", "Unit Test", "--dir", dir]));
  const request = JSON.parse(fs.readFileSync(output.request_path, "utf8"));
  const privateKey = JSON.parse(fs.readFileSync(output.private_key_path, "utf8"));

  assert.equal(request.name, "Unit Test");
  assert.deepEqual(Object.keys(request.recipient_public_key_jwk).sort(), ["e", "kty", "n"]);
  assert.equal(request.recipient_public_key_jwk.kty, "RSA");
  assert.equal(request.recipient_public_key_jwk.e, "AQAB");
  assert.equal(Boolean(privateKey.d), true);
  assert.equal(output.recipient_public_key_jwk.d, undefined);
});

test("skill documents connector install and retry preflight", () => {
  const skill = fs.readFileSync(SKILL, "utf8");

  assert.match(skill, /tool_search/);
  assert.match(skill, /tool_suggest/);
  assert.match(skill, /connector_2de447f3f15448ebab48783d7e4f5d81/);
  assert.match(skill, /retry `tool_search`/);
});

test("plugin and app tiles use the same OpenAI Platform logo", (t) => {
  if (!fs.existsSync(APP_ICON)) {
    t.skip("monorepo OpenAI Platform app icon is not available in this repository");
    return;
  }

  assert.deepEqual(fs.readFileSync(PLUGIN_ICON), fs.readFileSync(APP_ICON));
});

test("skill asks before building API-backed apps when any usable key exists", () => {
  const skill = fs.readFileSync(SKILL, "utf8");
  const description = skill.match(/description: (.*)/)?.[1] ?? "";

  assert.match(
    description,
    /Use for building, running, testing, debugging, or configuring apps, UIs, scripts, CLIs, generators, and tools that use AI/,
  );
  assert.match(
    description,
    /including AI-powered apps, apps that generate output with AI, and user-input-driven AI features\./,
  );
  assert.match(
    description,
    /Treat unspecified AI in build requests as OpenAI API usage unless the user names another provider or says not to use OpenAI\./,
  );
  assert.match(
    description,
    /First inspect credentials safely, then ask whether to reuse an existing key or create one before API-dependent implementation\./,
  );
  assert.doesNotMatch(
    description,
    /Do not use|docs-only|static UI|one-off content/,
  );
  assert.match(
    skill,
    /Use this skill as the credential gate for API-backed work, not as the app,\s+docs, or frontend implementation skill\./,
  );
  assert.match(
    skill,
    /Codex will build, implement, run, test, debug, or configure an app, script,\s+CLI, generator, UI, or tool that calls the OpenAI API, even before a live\s+request and even if a usable key already exists\./,
  );
  assert.match(
    skill,
    /The user asks Codex to build, implement, run, or configure an app, script, CLI,\s+generator, or tool that uses AI to produce outputs from user input\./,
  );
  assert.match(
    skill,
    /The user asks for an AI-powered app or UI that generates output from one or\s+more input fields, forms, prompts, files, or other user-provided values\./,
  );
  assert.match(
    skill,
    /The user says "using AI" in an app\/script\/build request and does not name a\s+different provider\./,
  );
  assert.match(
    skill,
    /The user only wants documentation, citations, model or API guidance,\s+conceptual explanation, or code examples without asking Codex to build, run,\s+configure, or debug an API-backed artifact\./,
  );
  assert.match(
    skill,
    /The user asks for a static frontend, visual mockup, design concept, or\s+placeholder UI with no API-backed behavior\./,
  );
  assert.match(
    skill,
    /The user only asks Codex to write a one-off output directly and no app,\s+script, generator, or API-backed tool is being built or run\./,
  );
  assert.match(
    skill,
    /The user names a different AI provider for the artifact\./,
  );
  assert.match(
    skill,
    /When another implementation skill also applies, this skill runs first only to\s+inspect credentials safely and send the credential decision message\./,
  );
  assert.match(
    skill,
    /Do not use\s+this skill to design the UI, generate visual concepts, choose app architecture,\s+inspect API examples, write code, or run smoke tests before the credential gate\s+is resolved\./,
  );
  assert.match(
    skill,
    /For API-backed app or UI requests, this credential gate takes precedence over\s+design-first and implementation-first workflows, including\s+`build-web-apps:frontend-app-builder`, until the reuse-existing-key vs\s+create-new-key decision is resolved\./,
  );
  assert.match(
    skill,
    /After the user answers the credential decision, continue with the appropriate\s+implementation, docs, or frontend skill for the actual build\./,
  );
  assert.match(
    skill,
    /before building, implementing, running, testing, debugging, or\s+configuring an app or script that calls the OpenAI API, ask up front\s+whether to reuse an existing usable key or create a new one/,
  );
  assert.match(
    skill,
    /if no usable key exists, ask whether to create one before building\s+the rest of the app/,
  );
  assert.match(
    skill,
    /ask this up-front question even when Codex has not yet made a live request;\s+do not defer it until verification or smoke testing/,
  );
  assert.match(
    skill,
    /do not silently reuse a detected key for implementation, verification,\s+smoke tests, or other live requests just because the user did not ask about\s+credentials/,
  );
  assert.match(
    skill,
    /Before creating a key or writing any secret, ask for explicit confirmation in\s+a standalone user-facing message that states the action, keeps supporting\s+detail in separate bullets, then wait\./,
  );
  assert.match(
    skill,
    /Before writing, confirm\s+the destination file\/env var\./,
  );
  assert.match(
    skill,
    /If creation is still needed and the user has not already explicitly asked for\s+a new key, ask whether to create one\./,
  );
});

test("skill forbids credential inspection that can print secrets", () => {
  const skill = fs.readFileSync(SKILL, "utf8");

  assert.match(
    skill,
    /Never inspect credentials with commands that can print secret values, such as\s+`cat \.env\*`, `grep OPENAI_API_KEY \.env\*`, or `rg OPENAI_API_KEY \.env\*`\./,
  );
  assert.match(
    skill,
    /inspect env files only with no-output checks that reveal presence\/absence,\s+never with commands that echo matching lines or whole files/,
  );
});

test("skill makes the key-choice gate impossible to miss", () => {
  const skill = fs.readFileSync(SKILL, "utf8");
  const workflowChoiceGate = skill.slice(
    skill.indexOf("2. Based on that inspection:"),
    skill.indexOf("3. If creation is still needed"),
  );
  const credentialDecisionMessages = skill.slice(
    skill.indexOf("## Credential Decision Messages"),
    skill.indexOf("## Workflow"),
  );
  const apiUseCopy = "OpenAI API will power the app, script, or project";

  assert.match(skill, /## Mandatory First Step/);
  assert.match(
    skill,
    /Before editing, testing, running, debugging, or configuring any code that calls\s+the OpenAI API:\s+1\. Inspect for a usable `OPENAI_API_KEY` without printing it\.\s+2\. Unless the user explicitly asked for a new key, ask whether to reuse an\s+existing key or create a new one\. If none exists, ask whether to create one\.\s+3\. Stop until the user answers\./,
  );
  assert.match(
    skill,
    /This applies even if:\s+- a usable key already exists\s+- no live API call will be made\s+- no secret will be written\s+- the task is "just create a script"/,
  );
  assert.match(
    skill,
    /Finding an existing key is not permission to proceed\. It only changes the\s+question you ask\./,
  );
  assert.match(
    skill,
    /The credential decision is a hard stop\. Before the user answers, do not create\s+directories, scaffold files, draft implementation plans, wire API-dependent\s+code, run smoke tests, or give placeholder\/manual key setup instructions\./,
  );
  assert.match(
    skill,
    /The\s+only allowed pre-gate work is safe repo convention discovery and credential\s+presence checks that do not print secrets\./,
  );
  assert.match(
    workflowChoiceGate,
    /for tasks that will call the OpenAI API, when asking this up-front question,\s+mention that the OpenAI API will power the app, script, or project before\s+mentioning whether an existing key was found in the environment or local\s+env files/,
  );
  assert.match(
    workflowChoiceGate,
    /after asking the up-front credential question, stop; do not include an\s+app plan, file list, code sketch, manual `OPENAI_API_KEY` instructions, or\s+fallback placeholder setup in the same response/,
  );
  assert.ok(
    workflowChoiceGate.indexOf(apiUseCopy) <
      workflowChoiceGate.indexOf("existing key was found in the environment"),
  );
  assert.match(skill, /## Credential Decision Messages/);
  assert.match(
    credentialDecisionMessages,
    /After inspecting credentials, the next user-facing message must be the\s+credential decision message\./,
  );
  assert.match(
    credentialDecisionMessages,
    /Do not send interim user-facing messages about env\s+files, key presence, API docs, file plans, implementation shape, or setup\s+instructions before this decision\./,
  );
  assert.match(
    credentialDecisionMessages,
    /Existing usable key found, and the user did not explicitly ask for a new key:\s+make clear that the OpenAI API will power the app, script, or project, say\s+that an existing usable `OPENAI_API_KEY` was found without revealing it, then\s+ask whether to reuse that key or create a new one\./,
  );
  assert.match(
    credentialDecisionMessages,
    /No usable key found: make clear that the OpenAI API will power the app,\s+script, or project, say that no usable `OPENAI_API_KEY` was found, then ask\s+whether to create one securely\./,
  );
  assert.match(
    credentialDecisionMessages,
    /User explicitly asked for a new key: skip the reuse question and use the\s+key-creation confirmation message below\./,
  );
  assert.match(
    credentialDecisionMessages,
    /After sending the credential decision message, stop until the user answers\./,
  );
  assert.doesNotMatch(skill, /first state exactly:\s+"I will use the OpenAI API to power this app\."/);
  assert.doesNotMatch(skill, /Use `app` in this sentence even when the user says script or project/);
});

test("skill documents a prominent confirmation prompt when key creation is needed", () => {
  const skill = fs.readFileSync(SKILL, "utf8");

  assert.match(
    skill,
    /\*\*I need to create an OpenAI API key for this project\. Want me to set it up for you\?\*\*/,
  );
  assert.match(skill, /<repo name> Codex/);
  assert.match(skill, /<confirmed env file path>/);
  assert.match(
    skill,
    /Reply yes to continue with this setup, or suggest a different one\./,
  );
  assert.match(
    skill,
    /Use that confirmation sentence exactly as written: no bullet, no backticks\s+around `yes`, and no rewritten second clause\./,
  );
  assert.doesNotMatch(
    skill,
    /-\s+Reply yes to continue with this setup, or suggest a different one\./,
  );
  assert.match(skill, /use one bold confirmation\s+line, short bullets/);
  assert.doesNotMatch(skill, /When Markdown is available/);
  assert.match(skill, /single\s+long sentence that buries the decision point/);
});

test("skill documents the final summary bullet after creating a key", () => {
  const skill = fs.readFileSync(SKILL, "utf8");

  assert.match(skill, /## Final Summary/);
  assert.match(
    skill,
    /After successfully creating and writing a new key, include this bullet in the final summary, replacing `<key name>` with the created key name:/,
  );
  assert.match(
    skill,
    /- I created an API Key named `<key name>` to call OpenAI APIs\. Manage OpenAI API use on \[platform\.openai\.com\]\(https:\/\/platform\.openai\.com\)\./,
  );
  assert.match(skill, /Keep the rest of the summary to safe metadata only\./);
  assert.match(skill, /Do not reveal the key value\./);
});

test("eval matrix includes two-field joke app use case", () => {
  const evals = fs.readFileSync(EVALS, "utf8");

  assert.match(evals, /### K5 - Two-field joke app/);
  assert.match(
    evals,
    /build an app that generates jokes using AI when i input 2 fields\. the joke should use those fields/,
  );
  assert.match(
    evals,
    /should invoke the `openai-platform-api-key` skill even though the user did not\s+mention keys/,
  );
  assert.match(
    evals,
    /should stop at the credential decision point until the user answers and should not\s+require a two-field app plan or implementation in the same rollout/,
  );
  assert.match(
    evals,
    /if the rollout proceeds after a confirmed key decision, the app plan or implementation\s+should collect two user input fields and send both fields into the AI joke-generation request/,
  );
});

test("openai-docs defers to API key skill for implementation tasks", (t) => {
  const docsSkillPaths = [OPENAI_DOCS_SKILL, APPLIED_OPENAI_DOCS_SKILL].filter(
    fs.existsSync,
  );
  if (docsSkillPaths.length === 0) {
    t.skip("monorepo OpenAI docs skill paths are not available in this repository");
    return;
  }

  for (const docsSkillPath of docsSkillPaths) {
    const docsSkill = fs.readFileSync(docsSkillPath, "utf8");

    assert.match(
      docsSkill,
      /For API-backed implementation tasks, defer to openai-platform-api-key first when available\./,
    );
    assert.match(
      docsSkill,
      /use `openai-platform-api-key` first when it is\s+available\. After that credential gate is resolved, return here for current docs\s+as needed\./,
    );
    assert.match(
      docsSkill,
      /Use this skill directly for docs-only questions, citations, model\/API guidance,\s+conceptual explanations, and examples that do not require building or running an\s+API-backed artifact\./,
    );
  }
});

test("decrypt writes the API key to the env file without printing it", async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(dir, ".env.local");
  const decryptOutput = runScript([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--env-name",
    "OPENAI_API_KEY",
    "--workspace",
    dir,
  ]);

  assert.equal(decryptOutput.includes(SECRET), false);
  assert.equal(fs.readFileSync(target, "utf8"), `OPENAI_API_KEY=${SECRET}\n`);
});

test("decrypt updates an existing env var without printing the API key", async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(dir, ".env.local");
  fs.writeFileSync(target, "OTHER=value\nOPENAI_API_KEY=old\n");

  const decryptOutput = JSON.parse(
    runScript([
      "decrypt",
      "--private-key",
      output.private_key_path,
      "--ciphertext",
      ciphertext,
      "--target",
      target,
      "--workspace",
      dir,
    ]),
  );

  assert.equal(decryptOutput.updated_existing, true);
  assert.equal(decryptOutput.wrote_plaintext_to_stdout, false);
  assert.equal(fs.readFileSync(target, "utf8"), `OTHER=value\nOPENAI_API_KEY=${SECRET}\n`);
});

test("decrypt tightens permissions on an existing env file", async () => {
  if (process.platform === "win32") {
    return;
  }

  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(dir, ".env.local");
  fs.writeFileSync(target, "OPENAI_API_KEY=old\n", { mode: 0o644 });
  fs.chmodSync(target, 0o644);

  runScript([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.equal(fs.statSync(target).mode & 0o777, 0o600);
});

test("decrypt preserves export when updating an exported env var", async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(dir, ".env.local");
  fs.writeFileSync(target, "OTHER=value\nexport OPENAI_API_KEY=old\n");

  const decryptOutput = JSON.parse(
    runScript([
      "decrypt",
      "--private-key",
      output.private_key_path,
      "--ciphertext",
      ciphertext,
      "--target",
      target,
      "--workspace",
      dir,
    ]),
  );

  assert.equal(decryptOutput.updated_existing, true);
  assert.equal(decryptOutput.wrote_plaintext_to_stdout, false);
  assert.equal(fs.readFileSync(target, "utf8"), `OTHER=value\nexport OPENAI_API_KEY=${SECRET}\n`);
});

test("decrypt rejects unsafe plaintext before writing env files", async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(
    output.recipient_public_key_jwk,
    "sk-proj-safe\nOTHER=value",
  );
  const target = path.join(dir, ".env.local");

  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /not a safe OpenAI API key literal/);
  assert.equal(fs.existsSync(target), false);
});

test("decrypt rejects symlink env targets without writing through them", async () => {
  if (process.platform === "win32") {
    return;
  }

  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-outside-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(dir, ".env.local");
  const symlinkDestination = path.join(outsideDir, "tracked-file.ts");
  fs.writeFileSync(symlinkDestination, "ORIGINAL\n");
  fs.symlinkSync(symlinkDestination, target);

  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /symlink target/);
  assert.equal(fs.readFileSync(symlinkDestination, "utf8"), "ORIGINAL\n");
});

test("decrypt rejects hard-linked env targets without writing through them", async () => {
  if (process.platform === "win32") {
    return;
  }

  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-outside-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(dir, ".env.local");
  const hardlinkDestination = path.join(outsideDir, "tracked-file.ts");
  fs.writeFileSync(hardlinkDestination, "ORIGINAL\n");
  try {
    fs.linkSync(hardlinkDestination, target);
  } catch {
    return;
  }

  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /hard-linked target/);
  assert.equal(fs.readFileSync(hardlinkDestination, "utf8"), "ORIGINAL\n");
});

test("decrypt rejects targets outside the selected workspace", async () => {
  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-outside-"));
  const output = JSON.parse(runScript(["prepare", "--dir", workspace]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const target = path.join(outsideDir, ".env.local");

  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--workspace",
    workspace,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /inside the workspace/);
  assert.equal(fs.existsSync(target), false);
});

test("decrypt rejects symlinked parent directories that escape the workspace", async () => {
  if (process.platform === "win32") {
    return;
  }

  const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-outside-"));
  const output = JSON.parse(runScript(["prepare", "--dir", workspace]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const linkedParent = path.join(workspace, "linked-parent");
  const target = path.join(linkedParent, ".env.local");
  fs.symlinkSync(outsideDir, linkedParent, "dir");

  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    ciphertext,
    "--target",
    target,
    "--workspace",
    workspace,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /inside the workspace/);
  assert.equal(fs.existsSync(path.join(outsideDir, ".env.local")), false);
});

test("decrypt treats ciphertext values that start with hyphens as values", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const target = path.join(dir, ".env.local");
  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    "--AA",
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /Failed to decrypt encrypted API key/);
  assert.doesNotMatch(result.stderr, /Provide --ciphertext or --encrypted-result/);
});

test("decrypt rejects unknown options and missing option values", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const target = path.join(dir, ".env.local");

  const unknown = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    "abc",
    "--target",
    target,
    "--workspace",
    dir,
    "--envname",
    "FOO",
  ]);
  assert.notEqual(unknown.status, 0);
  assert.match(unknown.stderr, /Unknown option: --envname/);

  const missing = runScriptFailure(["prepare", "--dir", "--name", "Unit Test"]);
  assert.notEqual(missing.status, 0);
  assert.match(missing.stderr, /Missing value for --dir/);
});

test("decrypt rejects impossible base64url lengths before decrypting", () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const target = path.join(dir, ".env.local");
  const result = runScriptFailure([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--ciphertext",
    "a",
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /Encrypted ciphertext must be base64url/);
});

test("decrypt accepts a structured connector result file", async () => {
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openai-platform-helper-test-"));
  const output = JSON.parse(runScript(["prepare", "--dir", dir]));
  const ciphertext = await encryptWithPublicJwk(output.recipient_public_key_jwk, SECRET);
  const encryptedResultPath = path.join(dir, "connector-result.json");
  const target = path.join(dir, ".env.local");
  fs.writeFileSync(
    encryptedResultPath,
    JSON.stringify({
      structuredContent: {
        encrypted_api_key: {
          version: 1,
          ciphertext,
        },
      },
    }),
  );

  const decryptOutput = runScript([
    "decrypt",
    "--private-key",
    output.private_key_path,
    "--encrypted-result",
    encryptedResultPath,
    "--target",
    target,
    "--workspace",
    dir,
  ]);

  assert.equal(decryptOutput.includes(SECRET), false);
  assert.equal(fs.readFileSync(target, "utf8"), `OPENAI_API_KEY=${SECRET}\n`);
});