Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions src/lib/BrewCleanup.svelte
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@
try {
judgment = await api.judgeBrewCleanup();
} catch (e) {
error = String(e);
error = "Homebrew 정리 계획을 만들지 못했습니다.";
} finally {
planning = false;
}
}

function approvalGuidance(): string {
if (!judgment || judgment.verdict !== "safe") return "";
if (!judgment.calibration || judgment.calibration.judgment_id !== judgment.judgment_id) {
if (!calibrationMatchesJudgment(judgment)) {
return "이 정확한 LLM 판정에 연결된 fast-mlsirm calibration이 필요합니다.";
}
if (!judgment.calibration.passed) {
if (!calibrationPassed(judgment)) {
return "fast-mlsirm Judge calibration이 통과하지 않아 실행할 수 없습니다.";
}
if (confirmationPhrase.trim() !== judgment.exact_approval_phrase) {
Expand All @@ -52,15 +52,21 @@
function executionReady(): boolean {
return judgment !== null
&& judgment.verdict === "safe"
&& judgment.calibration !== undefined
&& judgment.calibration.judgment_id === judgment.judgment_id
&& judgment.calibration.passed
&& calibrationPassed(judgment)
&& confirmationPhrase.trim() === judgment.exact_approval_phrase
&& rationale.trim().length > 0
&& !executing
&& execution === null;
}

function calibrationMatchesJudgment(value: api.BrewCleanupJudgment): boolean {
return value.calibration?.judgment_id === value.judgment_id;
}

function calibrationPassed(value: api.BrewCleanupJudgment): boolean {
return calibrationMatchesJudgment(value) && value.calibration?.passed === true;
}

async function executeCleanup() {
if (!judgment || !executionReady()) return;
const okay = await confirm(
Expand All @@ -82,7 +88,7 @@
rationale.trim(),
);
} catch (e) {
error = String(e);
error = "Homebrew 정리를 실행하지 못했습니다.";
} finally {
judgment = null;
confirmationPhrase = "";
Expand Down Expand Up @@ -111,8 +117,11 @@
<p class="fingerprint">계획 지문: {report.plan_fingerprint}</p>
<p class="fingerprint">실행 예정: brew cleanup --prune-prefix</p>
{#if report.calibration}
<p class:success={report.calibration.passed} class:warning={!report.calibration.passed}>
Judge calibration ({report.calibration.engine}): {report.calibration.passed ? "통과" : "실패"}
<p class:success={calibrationPassed(report)} class:warning={!calibrationPassed(report)}>
Judge calibration ({report.calibration.engine}):
{!calibrationMatchesJudgment(report)
? "현재 판정과 불일치"
: report.calibration.passed ? "통과" : "실패"}
Comment thread
seonghobae marked this conversation as resolved.
· 표본 {report.calibration.sample_count}개 · 일치율 {Math.round(report.calibration.exact_agreement * 100)}%
</p>
{:else}
Expand Down Expand Up @@ -144,15 +153,23 @@
{/if}

{#if execution}
<p class:success={execution.status_code === 0} class:error={execution.status_code !== 0}>
{execution.executed ? `실행 완료 (종료 코드 ${execution.status_code})` : "실행되지 않음"}
<p
class:success={execution.executed && execution.status_code === 0}
class:error={execution.executed && execution.status_code !== 0}
class:warning={!execution.executed}
>
{execution.executed
? execution.status_code === 0
? `실행 성공 (종료 코드 ${execution.status_code})`
: `실행 실패 (종료 코드 ${execution.status_code})`
: "실행되지 않음"}
</p>
{#if execution.stdout}<pre>{execution.stdout}</pre>{/if}
{#if execution.stderr}<pre class="error">{execution.stderr}</pre>{/if}
{#if execution.record_path}
<p class="muted">감사 기록: {execution.record_path}</p>
{:else}
<p class="error" role="alert">명령 결과는 반환됐지만 감사 기록을 저장하지 못했습니다: {execution.record_error}</p>
<p class="error" role="alert">명령 결과는 반환됐지만 감사 기록을 저장하지 못했습니다.</p>
{/if}
{/if}
</div>
Expand Down
40 changes: 40 additions & 0 deletions src/lib/brewCleanupErrorPrivacyContract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");

function readSource(path: string): string {
return readFileSync(resolve(repositoryRoot, path), "utf8");
}

describe("BrewCleanup privacy-safe failure feedback", () => {
it("never renders arbitrary backend exception text", () => {
const source = readSource("src/lib/BrewCleanup.svelte");
const evictionSource = readSource("src/lib/IcloudLocalEviction.svelte");

expect(source).not.toContain("String(e)");
expect(source).not.toContain("record_error");
expect(source).not.toContain("저장하지 못했습니다: {");
expect(source).not.toMatch(/\{execution\.record_error\}/);
expect(evictionSource).not.toContain("String(e)");
expect(evictionSource).not.toContain("result_record_error");
expect(evictionSource).not.toMatch(/\{eviction\.result_record_error\}/);
expect(evictionSource).toContain("파일 선택 창을 열지 못했습니다.");
expect(evictionSource).toContain("iCloud 로컬 사본 상태를 확인하지 못했습니다.");
expect(evictionSource).toContain("iCloud 로컬 사본 축출을 실행하지 못했습니다.");
expect(source).toContain("Homebrew 정리 계획을 만들지 못했습니다.");
expect(source).toContain("Homebrew 정리를 실행하지 못했습니다.");
expect(source).toContain('role=\"alert\"');
});

it("preserves the existing judgment and execution authority calls", () => {
const source = readSource("src/lib/BrewCleanup.svelte");

expect(source).toContain("api.judgeBrewCleanup()");
expect(source).toContain("api.executeBrewCleanup(");
expect(source).toContain("submittedJudgment.plan_fingerprint");
expect(source).toContain("submittedJudgment.judgment_id");
});
});
25 changes: 25 additions & 0 deletions src/lib/brewCleanupExecutionStatusContract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";

const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");

describe("BrewCleanup execution status contract", () => {
it("does not style a non-executed zero-status response as success", () => {
const source = readFileSync(resolve(repositoryRoot, "src/lib/BrewCleanup.svelte"), "utf8");

expect(source).toContain(
"class:success={execution.executed && execution.status_code === 0}",
);
expect(source).toContain(
"class:error={execution.executed && execution.status_code !== 0}",
);
expect(source).toContain("class:warning={!execution.executed}");
expect(source).toContain("실행 성공");
expect(source).toContain("실행 실패");
expect(source).toContain("실행되지 않음");
expect(source).toContain("execution.executed");
expect(source).not.toContain("class:success={execution.status_code === 0}");
});
});
26 changes: 26 additions & 0 deletions src/lib/brewCleanupSafetyUiContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ describe("Homebrew cleanup safety UX", () => {
expect(executeCleanup).toContain("judgment = null;");
});

it("distinguishes executed and non-executed result states", () => {
const source = readSource("src/lib/BrewCleanup.svelte");
const start = source.indexOf("{#if execution}");
const end = source.indexOf("\n {/if}\n </div>", start);
const executionResult = source.slice(start, end);

expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
expect(executionResult).toContain("execution.executed");
expect(executionResult).toContain("실행 성공 (종료 코드");
expect(executionResult).toContain("실행 실패 (종료 코드");
expect(executionResult).toContain("실행되지 않음");
expect(executionResult).not.toContain("실행 완료</p>");
});

it("normalizes the approval phrase and explains why execution is unavailable", () => {
const source = readSource("src/lib/BrewCleanup.svelte");

Expand All @@ -41,4 +56,15 @@ describe("Homebrew cleanup safety UX", () => {
expect(source).toContain("승인 문구가 일치하지 않습니다.");
expect(source).toContain("실행 사유를 입력하십시오.");
});

it("distinguishes a failed subprocess from a successful execution", () => {
const source = readSource("src/lib/BrewCleanup.svelte");

expect(source).toContain("execution.status_code === 0");
expect(source).toContain("실행 성공 (종료 코드");
expect(source).toContain("실행 실패 (종료 코드");
expect(source).not.toContain(
"execution.executed ? `실행 완료 (종료 코드 ${execution.status_code})`",
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
11 changes: 11 additions & 0 deletions src/lib/icloudLocalEvictionSafetyUiContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,15 @@ describe("iCloud local eviction safety UI", () => {
"File Provider 상태 증거가 완전하지 않습니다. 잠시 후 다시 판정하세요.",
]);
});

it("retains provider-aware progress and deduplicated blocker feedback", () => {
const source = readSource();

expect(source).toContain("function uploadLabel");
expect(source).toContain("업로드 중");
expect(source).toContain("function syncLabel");
expect(source).toContain("공급자 상태");
expect(source).toContain("planBlockerActions(plan.blockers");
expect(source).not.toContain("plan.blockers.join(\", \")");
});
});
Loading