mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8f9b0322 | ||
|
|
aeefceeaa2 | ||
|
|
3058886312 | ||
|
|
70d8610733 | ||
|
|
0f60ec41f1 | ||
|
|
2797667e04 | ||
|
|
f23aacdec9 | ||
|
|
8dc8b9f49f |
No files matched your search
@@ -27,7 +27,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
@@ -16,8 +16,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
recover:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
pr_number: ${{ github.event.inputs.pr_number || '' }}
|
||||
@@ -25,8 +25,8 @@ permissions:
|
||||
|
||||
jobs:
|
||||
detect-and-trigger:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
pr_number: ${{ github.event.inputs.pr_number || '' }}
|
||||
@@ -43,9 +43,9 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].8
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].10
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
@@ -87,9 +87,9 @@ jobs:
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
pr_number: ${{ github.event.inputs.pr_number || github.event.client_payload.pr_number || '' }}
|
||||
merge_method: ${{ github.event.inputs.merge_method || 'merge' }}
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
issues: write
|
||||
actions: read
|
||||
id-token: write
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].8
|
||||
- uses: MervinPraison/github-automation-template/.github/actions/[email protected].10
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
- name: Detect conflicting PRs and trigger Claude
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
|
||||
@@ -13,7 +13,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].8
|
||||
uses: MervinPraison/github-automation-template/.github/workflows/[email protected].10
|
||||
secrets: inherit
|
||||
with:
|
||||
hub_ref: v1.1.8
|
||||
hub_ref: v1.1.10
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Security Policy
|
||||
|
||||
We take the security of PraisonAIUI seriously and appreciate responsible
|
||||
disclosure of vulnerabilities.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes are applied to the latest released version of PraisonAIUI.
|
||||
Please make sure you are on the most recent release before reporting.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
**Please do not open a public issue for security vulnerabilities.**
|
||||
|
||||
Report privately through GitHub's **Private Vulnerability Reporting**:
|
||||
|
||||
1. Go to the [Security tab](https://github.com/MervinPraison/PraisonAIUI/security)
|
||||
of this repository.
|
||||
2. Click **"Report a vulnerability"** to open a private advisory.
|
||||
3. Include as much detail as you can:
|
||||
- Affected component(s) and version.
|
||||
- Steps to reproduce.
|
||||
- Impact assessment.
|
||||
- Any suggested fix or mitigation.
|
||||
|
||||
If you are unable to use private reporting, you may open a public issue that
|
||||
contains **no technical specifics** and requests a private channel, and a
|
||||
maintainer will follow up.
|
||||
|
||||
## What to Expect
|
||||
|
||||
- We aim to acknowledge new reports within a few business days.
|
||||
- We will keep you informed as we investigate and work on a fix.
|
||||
- Once a fix is released, we are happy to credit you in the advisory (unless
|
||||
you prefer to remain anonymous).
|
||||
|
||||
Thank you for helping keep PraisonAIUI and its users safe.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "aiui"
|
||||
version = "0.3.145"
|
||||
version = "0.3.146"
|
||||
description = "YAML-driven website generator - CLI and compiler"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""Version information."""
|
||||
|
||||
__version__ = "0.3.145"
|
||||
__version__ = "0.3.146"
|
||||
@@ -44,6 +44,43 @@ def _icon(symbol: str, fallback: str) -> str:
|
||||
return symbol if _supports_unicode() else fallback
|
||||
|
||||
|
||||
def _load_env_files() -> int:
|
||||
"""Load environment variables from the standard PraisonAI ``.env`` files.
|
||||
|
||||
Mirrors the PraisonAI CLI convention so ``env:VAR`` references (e.g. in
|
||||
Channels config) resolve when the server was started without the shell
|
||||
exporting the tokens. Reads ``~/.praisonai/.env`` first, then a ``.env`` in
|
||||
the current working directory. Existing process env vars are never
|
||||
overridden, and parsing is intentionally dependency-free (no python-dotenv).
|
||||
|
||||
Returns the number of variables newly set into ``os.environ``.
|
||||
"""
|
||||
import os
|
||||
|
||||
loaded = 0
|
||||
for env_path in (Path.home() / ".praisonai" / ".env", Path.cwd() / ".env"):
|
||||
try:
|
||||
if not env_path.is_file():
|
||||
continue
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
if line.startswith("export "):
|
||||
line = line[len("export "):]
|
||||
key, value = line.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip().strip('"').strip("'")
|
||||
if key and key not in os.environ:
|
||||
os.environ[key] = value
|
||||
loaded += 1
|
||||
except Exception:
|
||||
# Never let env loading break startup — resolution falls back to
|
||||
# the existing process environment.
|
||||
continue
|
||||
return loaded
|
||||
|
||||
|
||||
app = typer.Typer(
|
||||
name="aiui",
|
||||
help="PraisonAIUI - YAML-driven website generator",
|
||||
@@ -1380,6 +1417,17 @@ def run(
|
||||
console.print(f"[red]Error:[/red] App file not found: {app_file}")
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
# Auto-load standard PraisonAI env files so `env:VAR` references (e.g. in
|
||||
# Channels config) resolve even when the shell did not export the tokens.
|
||||
# This matches the PraisonAI CLI convention and fixes the "Connected but
|
||||
# bot never replies" class of issues caused by empty env-resolved tokens.
|
||||
_loaded = _load_env_files()
|
||||
if _loaded:
|
||||
console.print(
|
||||
f"[green]{_icon('✓', 'OK')}[/green] Loaded {_loaded} environment "
|
||||
f"variable(s) from PraisonAI config"
|
||||
)
|
||||
|
||||
is_yaml = app_file.suffix in (".yaml", ".yml")
|
||||
|
||||
if is_yaml:
|
||||
|
||||
@@ -1,65 +1,154 @@
|
||||
/**
|
||||
* Approvals View — execution approval queue
|
||||
* API: /api/approvals
|
||||
*
|
||||
* Pending items render as an Approval Card (human-in-the-loop): risk, question,
|
||||
* optional preset options / custom answer / confidence copy — surfaced only when
|
||||
* the API supplies them, so the card stays a plain approve/deny prompt otherwise.
|
||||
*/
|
||||
import { isSkillApproval } from '/plugins/views/_helpers.js';
|
||||
import { isSkillApproval, esc, emptyState } from '/plugins/views/_helpers.js';
|
||||
|
||||
const RISK = {
|
||||
low: { icon: '✅', color: '#22c55e' },
|
||||
medium: { icon: '⚠️', color: '#eab308' },
|
||||
high: { icon: '🟠', color: '#f97316' },
|
||||
critical: { icon: '🔴', color: '#ef4444' },
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalise a raw /api/approvals item into an ApprovalPromptModel-shaped object.
|
||||
* Keeps the field-mapping in one place; only carries what the API actually sends.
|
||||
*/
|
||||
function toApprovalModel(a) {
|
||||
const id = a.id || a.approval_id || '';
|
||||
const risk = String(a.risk_level || '').toLowerCase();
|
||||
const options = Array.isArray(a.options)
|
||||
? a.options.map(o => (typeof o === 'string' ? { value: o, label: o } : { value: o.value ?? o.id ?? o.label, label: o.label ?? String(o.value ?? o.id) }))
|
||||
: [];
|
||||
const confidence = typeof a.confidence === 'number' ? Math.round(a.confidence * (a.confidence <= 1 ? 100 : 1)) : null;
|
||||
return {
|
||||
id,
|
||||
title: a.tool_name || a.action || 'Unknown',
|
||||
question: a.reason || a.description || a.question || '',
|
||||
agent: a.agent_name || '',
|
||||
risk,
|
||||
riskMeta: RISK[risk] || { icon: '❓', color: 'var(--db-text-dim)' },
|
||||
createdAt: a.created_at ? new Date(a.created_at * 1000).toLocaleString() : '',
|
||||
options,
|
||||
allowCustom: a.allow_custom_answer === true || (options.length === 0 && a.custom_answer !== false && !!a.question),
|
||||
confidence,
|
||||
isSkill: isSkillApproval(a),
|
||||
};
|
||||
}
|
||||
|
||||
function approvalCard(m) {
|
||||
const optionsHtml = m.options.length
|
||||
? `<div class="db-approval-options" role="radiogroup" aria-label="Response options" style="display:flex;flex-wrap:wrap;gap:6px;margin-top:10px">
|
||||
${m.options.map((o, i) => `
|
||||
<label style="display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:4px 10px;border:1px solid var(--db-border);border-radius:6px;cursor:pointer">
|
||||
<input type="radio" name="opt-${esc(m.id)}" value="${esc(o.value)}" ${i === 0 ? 'checked' : ''}> ${esc(o.label)}
|
||||
</label>`).join('')}
|
||||
</div>`
|
||||
: '';
|
||||
const customHtml = m.allowCustom
|
||||
? `<input class="approv-custom" data-id="${esc(m.id)}" type="text" placeholder="Custom answer (optional)"
|
||||
aria-label="Custom answer for ${esc(m.title)}"
|
||||
style="margin-top:10px;width:100%;padding:6px 10px;font-size:12px;background:var(--db-bg);color:var(--db-text);border:1px solid var(--db-border);border-radius:6px">`
|
||||
: '';
|
||||
const confidenceHtml = m.confidence != null
|
||||
? `<div style="font-size:11px;color:var(--db-text-dim);margin-top:6px">Model confidence: ${m.confidence}%</div>`
|
||||
: '';
|
||||
const skillHtml = m.isSkill
|
||||
? `<a href="/skills?tab=pending&approval_id=${encodeURIComponent(m.id)}" data-skill-studio="1" style="display:inline-block;margin-top:8px;font-size:11px;color:var(--db-accent);text-decoration:none">Skill write? Open Skills Studio →</a>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="db-card db-approval-card" data-id="${esc(m.id)}" role="group"
|
||||
aria-label="Approval request: ${esc(m.title)}"
|
||||
style="margin-bottom:12px;padding:16px 20px;border-left:3px solid ${m.riskMeta.color}">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:14px">${m.riskMeta.icon} ${esc(m.title)}</div>
|
||||
${m.question ? `<div style="font-size:13px;color:var(--db-text);margin-top:6px">${esc(m.question)}</div>` : ''}
|
||||
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">Agent: ${esc(m.agent) || '—'}${m.risk ? ` · risk: ${esc(m.risk)}` : ''}</div>
|
||||
${m.createdAt ? `<div style="font-size:11px;color:var(--db-text-dim)">${esc(m.createdAt)}</div>` : ''}
|
||||
${optionsHtml}
|
||||
${customHtml}
|
||||
${confidenceHtml}
|
||||
${skillHtml}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0">
|
||||
<button class="approv-yes" data-id="${esc(m.id)}" aria-label="Approve ${esc(m.title)}"
|
||||
style="padding:6px 16px;background:rgba(34,197,94,0.15);color:#22c55e;border:1px solid rgba(34,197,94,0.3);border-radius:6px;cursor:pointer;font-size:12px">✓ Approve</button>
|
||||
<button class="approv-no" data-id="${esc(m.id)}" aria-label="Deny ${esc(m.title)}"
|
||||
style="padding:6px 16px;background:rgba(239,68,68,0.15);color:#ef4444;border:1px solid rgba(239,68,68,0.3);border-radius:6px;cursor:pointer;font-size:12px">✗ Deny</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
export async function render(container) {
|
||||
container.innerHTML = '<div class="db-loading"><div class="db-spinner"></div></div>';
|
||||
|
||||
let pending = [], history = [], policies = {};
|
||||
try { const r = await fetch('/api/approvals/pending'); pending = (await r.json()).approvals || []; } catch(e) {}
|
||||
try { const r = await fetch('/api/approvals/history'); history = (await r.json()).approvals || (await r.json()).history || []; } catch(e) {}
|
||||
try { const r = await fetch('/api/approvals/history'); const d = await r.json(); history = d.approvals || d.history || []; } catch(e) {}
|
||||
try { const r = await fetch('/api/approvals/policies'); policies = await r.json(); } catch(e) {}
|
||||
|
||||
const riskIcons = { low: '✅', medium: '⚠️', high: '🟠', critical: '🔴' };
|
||||
const models = pending.map(toApprovalModel);
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="db-columns" style="grid-template-columns:repeat(3,1fr);margin-bottom:24px">
|
||||
<div class="db-card"><div class="db-card-title">Pending</div><div class="db-card-value" style="color:#eab308">${pending.length}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Risk Threshold</div><div class="db-card-value" style="font-size:18px">${policies.risk_threshold || 'high'}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Pending</div><div class="db-card-value" style="color:#eab308">${models.length}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Risk Threshold</div><div class="db-card-value" style="font-size:18px">${esc(policies.risk_threshold || 'high')}</div></div>
|
||||
<div class="db-card"><div class="db-card-title">Auto-Approve Tools</div><div class="db-card-value" style="font-size:18px">${(policies.auto_approve_tools || []).length}</div></div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600">Pending Approvals</h3>
|
||||
<div id="approv-pending" style="margin-bottom:28px">
|
||||
${pending.length === 0 ? '<div class="db-viewer"><pre>No pending approvals</pre></div>' : ''}
|
||||
${models.length === 0 ? emptyState({ icon: '✅', title: 'No pending approvals', body: 'All clear — nothing needs your decision.' }) : models.map(approvalCard).join('')}
|
||||
</div>
|
||||
|
||||
<h3 style="margin:0 0 12px;font-size:15px;font-weight:600">Recent History</h3>
|
||||
<div id="approv-history"></div>
|
||||
`;
|
||||
|
||||
const pendingEl = container.querySelector('#approv-pending');
|
||||
pending.forEach(a => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'db-card';
|
||||
card.style.cssText = 'margin-bottom:10px;padding:16px 20px;display:flex;align-items:center;justify-content:space-between';
|
||||
const isSkill = isSkillApproval(a);
|
||||
card.innerHTML = `
|
||||
<div style="flex:1">
|
||||
<div style="font-weight:500;font-size:14px">${riskIcons[a.risk_level] || '❓'} ${a.tool_name || a.action || 'Unknown'}</div>
|
||||
<div style="font-size:12px;color:var(--db-text-dim);margin-top:4px">Agent: ${a.agent_name || '—'} · ${a.reason || ''}</div>
|
||||
<div style="font-size:11px;color:var(--db-text-dim)">${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}</div>
|
||||
${isSkill ? `<a href="/skills?tab=pending&approval_id=${encodeURIComponent(a.id || a.approval_id || '')}" data-skill-studio="1" style="font-size:11px;color:var(--db-accent);text-decoration:none">Skill write? Open Skills Studio →</a>` : ''}
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<button class="approv-yes" data-id="${a.id || a.approval_id}" style="padding:6px 16px;background:rgba(34,197,94,0.15);color:#22c55e;border:1px solid rgba(34,197,94,0.3);border-radius:6px;cursor:pointer;font-size:12px">✓ Approve</button>
|
||||
<button class="approv-no" data-id="${a.id || a.approval_id}" style="padding:6px 16px;background:rgba(239,68,68,0.15);color:#ef4444;border:1px solid rgba(239,68,68,0.3);border-radius:6px;cursor:pointer;font-size:12px">✗ Deny</button>
|
||||
</div>
|
||||
`;
|
||||
pendingEl.appendChild(card);
|
||||
});
|
||||
function collectExtra(id) {
|
||||
const card = container.querySelector(`.db-approval-card[data-id="${CSS.escape(id)}"]`);
|
||||
const body = {};
|
||||
if (!card) return body;
|
||||
const opt = card.querySelector(`input[name="opt-${CSS.escape(id)}"]:checked`);
|
||||
if (opt) body.option = opt.value;
|
||||
const custom = card.querySelector('.approv-custom');
|
||||
if (custom && custom.value.trim()) body.custom_answer = custom.value.trim();
|
||||
// Also surface the human's choice as `reason` — the only extra field the
|
||||
// approve/deny endpoints persist today — so the answer isn't dropped while
|
||||
// `option`/`custom_answer` stay for a future richer backend.
|
||||
const reason = body.custom_answer || body.option;
|
||||
if (reason != null && reason !== '') body.reason = String(reason);
|
||||
return body;
|
||||
}
|
||||
|
||||
container.querySelectorAll('.approv-yes').forEach(b => b.addEventListener('click', async () => { try { await fetch(`/api/approvals/${b.dataset.id}/approve`, {method:'POST'}); render(container); } catch(e){} }));
|
||||
container.querySelectorAll('.approv-no').forEach(b => b.addEventListener('click', async () => { try { await fetch(`/api/approvals/${b.dataset.id}/deny`, {method:'POST'}); render(container); } catch(e){} }));
|
||||
async function decide(id, action) {
|
||||
try {
|
||||
await fetch(`/api/approvals/${encodeURIComponent(id)}/${action}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(collectExtra(id)),
|
||||
});
|
||||
render(container);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
container.querySelectorAll('.approv-yes').forEach(b => b.addEventListener('click', () => decide(b.dataset.id, 'approve')));
|
||||
container.querySelectorAll('.approv-no').forEach(b => b.addEventListener('click', () => decide(b.dataset.id, 'deny')));
|
||||
|
||||
const historyEl = container.querySelector('#approv-history');
|
||||
(Array.isArray(history) ? history : []).slice(0, 20).forEach(h => {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'padding:8px 0;border-bottom:1px solid var(--db-border);font-size:13px;display:flex;justify-content:space-between';
|
||||
const approved = h.status === 'approved' || h.approved;
|
||||
div.innerHTML = `<span>${h.tool_name || h.action || '?'} — ${h.agent_name || ''}</span><span style="color:${approved ? '#22c55e' : '#ef4444'}">${approved ? '✓ approved' : '✗ denied'}</span>`;
|
||||
div.innerHTML = `<span>${esc(h.tool_name || h.action || '?')} — ${esc(h.agent_name || '')}</span><span style="color:${approved ? '#22c55e' : '#ef4444'}">${approved ? '✓ approved' : '✗ denied'}</span>`;
|
||||
historyEl.appendChild(div);
|
||||
});
|
||||
if (!history.length) historyEl.innerHTML = '<div style="font-size:13px;color:var(--db-text-dim)">No history</div>';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Unit tests for `_load_env_files` — auto-loading PraisonAI `.env` files.
|
||||
|
||||
Regression coverage for issue #286: `env:VAR` channel token references
|
||||
resolved to empty strings because `aiui run` never loaded the standard
|
||||
`~/.praisonai/.env` file, so bots showed "Connected" but never replied.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from praisonaiui.cli import _load_env_files
|
||||
|
||||
|
||||
def _write_env(path, body):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(body, encoding="utf-8")
|
||||
|
||||
|
||||
def test_loads_home_praisonai_env(tmp_path, monkeypatch):
|
||||
"""Variables in ~/.praisonai/.env are loaded into the process env."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False)
|
||||
|
||||
_write_env(tmp_path / ".praisonai" / ".env", 'TELEGRAM_BOT_TOKEN="123:AAtoken"\n')
|
||||
|
||||
loaded = _load_env_files()
|
||||
|
||||
assert loaded >= 1
|
||||
assert os.environ["TELEGRAM_BOT_TOKEN"] == "123:AAtoken"
|
||||
|
||||
|
||||
def test_does_not_override_existing_env(tmp_path, monkeypatch):
|
||||
"""An already-set process variable is never clobbered by the file."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.setenv("SLACK_BOT_TOKEN", "xoxb-from-shell")
|
||||
|
||||
_write_env(tmp_path / ".praisonai" / ".env", "SLACK_BOT_TOKEN=xoxb-from-file\n")
|
||||
|
||||
_load_env_files()
|
||||
|
||||
assert os.environ["SLACK_BOT_TOKEN"] == "xoxb-from-shell"
|
||||
|
||||
|
||||
def test_ignores_comments_blanks_and_export_prefix(tmp_path, monkeypatch):
|
||||
"""Comments/blank lines are skipped and `export ` prefixes are stripped."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("FOO", raising=False)
|
||||
monkeypatch.delenv("BAR", raising=False)
|
||||
|
||||
_write_env(
|
||||
tmp_path / ".praisonai" / ".env",
|
||||
"# a comment\n\nexport FOO=foo\nBAR='bar'\n",
|
||||
)
|
||||
|
||||
_load_env_files()
|
||||
|
||||
assert os.environ["FOO"] == "foo"
|
||||
assert os.environ["BAR"] == "bar"
|
||||
|
||||
|
||||
def test_missing_files_are_harmless(tmp_path, monkeypatch):
|
||||
"""No env files present → returns 0 and does not raise."""
|
||||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
assert _load_env_files() == 0
|
||||
Reference in new issue
Block a user