fix: UI contrast issues and ARM64 build failure

**UI Fixes (TDD approach - tests first, then implementation):**
- Resolution steps: improved text contrast (text-foreground vs muted)
- DocEditor preview: added text-foreground class for readability
- History page: fixed domain display (category field) with better contrast
- Audit Log: added expandable rows with View/Hide buttons to show transmitted data
- Dashboard & buttons: already had proper contrast with outline variant
- Export document: fixed missing title/content parameters in command signature

**Tests Added (13 new tests, all passing):**
- tests/unit/resolution.test.tsx - resolution steps contrast
- tests/unit/docEditor.test.tsx - preview mode and export buttons
- tests/unit/exportDocument.test.ts - export parameters validation
- tests/unit/history.test.tsx - domain display and filtering
- tests/unit/dashboard.test.tsx - refresh button visibility
- tests/unit/auditLog.test.tsx - data visibility and expandable rows
- tests/unit/setup.ts - added @testing-library/jest-dom matchers

**CI Fix:**
- Removed platform label from ARM64 build step (native agent, old Docker)

**Test Results:**
- Frontend: 38/38 passing 
- Backend: 64/64 passing 
- TypeScript: no errors 

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Shaun Arman 2026-04-03 08:05:58 -05:00
parent 03cc9914ad
commit 4c4ca40146
21 changed files with 7898 additions and 433 deletions

View File

@ -62,8 +62,6 @@ steps:
- name: build-linux-arm64
image: rust:1.88-slim
labels:
platform: linux/arm64
environment:
TARGET: aarch64-unknown-linux-gnu
when:

943
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,34 +18,35 @@
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2",
"@tauri-apps/plugin-stronghold": "^2",
"react": "^18",
"react-dom": "^18",
"react-router-dom": "^6",
"zustand": "^4",
"tailwindcss": "^3",
"lucide-react": "latest",
"react-diff-viewer-continued": "^3",
"react-markdown": "^9",
"remark-gfm": "^4",
"class-variance-authority": "^0.7",
"clsx": "^2",
"class-variance-authority": "^0.7"
"lucide-react": "latest",
"react": "^18",
"react-diff-viewer-continued": "^3",
"react-dom": "^18",
"react-markdown": "^9",
"react-router-dom": "^6",
"remark-gfm": "^4",
"tailwindcss": "^3",
"zustand": "^4"
},
"devDependencies": {
"@tauri-apps/cli": "^2",
"@vitejs/plugin-react": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16",
"@testing-library/user-event": "^14",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^4",
"@vitest/coverage-v8": "^2",
"@wdio/cli": "^9",
"@wdio/mocha-framework": "^9",
"autoprefixer": "^10",
"jsdom": "^26",
"postcss": "^8",
"typescript": "^5",
"vite": "^6",
"vitest": "^2",
"@testing-library/react": "^16",
"@testing-library/user-event": "^14",
"@vitest/coverage-v8": "^2",
"jsdom": "^26",
"webdriverio": "^9",
"@wdio/cli": "^9",
"@wdio/mocha-framework": "^9"
"webdriverio": "^9"
}
}

265
src-tauri/Cargo.lock generated
View File

@ -100,71 +100,12 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayref"
version = "0.3.9"
@ -563,39 +504,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clap"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "combine"
version = "4.6.7"
@ -955,17 +863,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "derive_more"
version = "0.99.20"
@ -2396,12 +2293,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "iterator-sorted"
version = "0.1.0"
@ -2629,7 +2520,7 @@ dependencies = [
"tar",
"ureq",
"vcpkg",
"zip 7.2.0",
"zip",
]
[[package]]
@ -3034,7 +2925,6 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.11.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@ -3050,18 +2940,6 @@ dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "objc2-osa-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
dependencies = [
"bitflags 2.11.0",
"objc2",
"objc2-app-kit",
"objc2-foundation",
]
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@ -3106,12 +2984,6 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
@ -3200,20 +3072,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "osakit"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2",
"objc2-foundation",
"objc2-osa-kit",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "owned_ttf_parser"
version = "0.19.0"
@ -4067,20 +3925,15 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
"hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@ -4225,18 +4078,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
@ -4247,33 +4088,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.9"
@ -5240,21 +5054,6 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-cli"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28e78fb2c09a81546bcd376d34db4bda5769270d00990daa9f0d6e7ac1107e25"
dependencies = [
"clap",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.6.0"
@ -5361,39 +5160,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61"
dependencies = [
"base64 0.22.1",
"dirs 6.0.0",
"flate2",
"futures-util",
"http",
"infer",
"log",
"minisign-verify",
"osakit",
"percent-encoding",
"reqwest 0.13.2",
"rustls",
"semver",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-plugin",
"tempfile",
"thiserror 2.0.18",
"time",
"tokio",
"url",
"windows-sys 0.60.2",
"zip 4.6.1",
]
[[package]]
name = "tauri-runtime"
version = "2.10.1"
@ -5538,13 +5304,11 @@ dependencies = [
"sha2",
"tauri",
"tauri-build",
"tauri-plugin-cli",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-http",
"tauri-plugin-shell",
"tauri-plugin-stronghold",
"tauri-plugin-updater",
"thiserror 1.0.69",
"tokio",
"tokio-test",
@ -6159,12 +5923,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.22.0"
@ -6465,15 +6223,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.6"
@ -7339,18 +7088,6 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "zip"
version = "4.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
dependencies = [
"arbitrary",
"crc32fast",
"indexmap 2.13.0",
"memchr",
]
[[package]]
name = "zip"
version = "7.2.0"

File diff suppressed because one or more lines are too long

View File

@ -2324,24 +2324,6 @@
"Identifier": {
"description": "Permission identifier",
"oneOf": [
{
"description": "Allows reading the CLI matches\n#### This default permission set includes:\n\n- `allow-cli-matches`",
"type": "string",
"const": "cli:default",
"markdownDescription": "Allows reading the CLI matches\n#### This default permission set includes:\n\n- `allow-cli-matches`"
},
{
"description": "Enables the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:allow-cli-matches",
"markdownDescription": "Enables the cli_matches command without any pre-configured scope."
},
{
"description": "Denies the cli_matches command without any pre-configured scope.",
"type": "string",
"const": "cli:deny-cli-matches",
"markdownDescription": "Denies the cli_matches command without any pre-configured scope."
},
{
"description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`",
"type": "string",
@ -6373,60 +6355,6 @@
"type": "string",
"const": "stronghold:deny-save-store-record",
"markdownDescription": "Denies the save_store_record command without any pre-configured scope."
},
{
"description": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`",
"type": "string",
"const": "updater:default",
"markdownDescription": "This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n\n#### This default permission set includes:\n\n- `allow-check`\n- `allow-download`\n- `allow-install`\n- `allow-download-and-install`"
},
{
"description": "Enables the check command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-check",
"markdownDescription": "Enables the check command without any pre-configured scope."
},
{
"description": "Enables the download command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download",
"markdownDescription": "Enables the download command without any pre-configured scope."
},
{
"description": "Enables the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-download-and-install",
"markdownDescription": "Enables the download_and_install command without any pre-configured scope."
},
{
"description": "Enables the install command without any pre-configured scope.",
"type": "string",
"const": "updater:allow-install",
"markdownDescription": "Enables the install command without any pre-configured scope."
},
{
"description": "Denies the check command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-check",
"markdownDescription": "Denies the check command without any pre-configured scope."
},
{
"description": "Denies the download command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download",
"markdownDescription": "Denies the download command without any pre-configured scope."
},
{
"description": "Denies the download_and_install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-download-and-install",
"markdownDescription": "Denies the download_and_install command without any pre-configured scope."
},
{
"description": "Denies the install command without any pre-configured scope.",
"type": "string",
"const": "updater:deny-install",
"markdownDescription": "Denies the install command without any pre-configured scope."
}
]
},

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,7 @@ export function DocEditor({ content, onChange, version, updatedAt, onExport }: D
placeholder="Start writing your document..."
/>
) : (
<div className="p-4 prose prose-sm dark:prose-invert max-w-none">
<div className="p-4 prose prose-sm dark:prose-invert max-w-none text-foreground">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
)}

View File

@ -329,8 +329,8 @@ export const generatePostmortemCmd = (issueId: string) =>
export const updateDocumentCmd = (docId: string, contentMd: string) =>
invoke<Document_>("update_document", { docId, contentMd });
export const exportDocumentCmd = (docId: string, format: string, outputDir: string) =>
invoke<string>("export_document", { docId, format, outputDir });
export const exportDocumentCmd = (docId: string, title: string, contentMd: string, format: string, outputDir: string) =>
invoke<string>("export_document", { title, contentMd, format, outputDir });
// ─── Ollama & System ──────────────────────────────────────────────────────────

View File

@ -174,8 +174,8 @@ export default function History() {
className="border-b last:border-0 hover:bg-accent/50 transition-colors"
>
<td className="px-4 py-3 text-sm font-medium">{issue.title}</td>
<td className="px-4 py-3 text-sm text-muted-foreground capitalize">
{issue.domain}
<td className="px-4 py-3 text-sm text-foreground/80 capitalize">
{issue.category}
</td>
<td className="px-4 py-3">
<Badge variant={severityVariant(issue.severity)}>

View File

@ -52,7 +52,7 @@ export default function Postmortem() {
const handleExport = async (format: "md" | "pdf" | "docx") => {
if (!doc) return;
try {
await exportDocumentCmd(doc.id, format, ".");
await exportDocumentCmd(doc.id, doc.title, content, format, ".");
} catch (err) {
setError(String(err));
}

View File

@ -53,7 +53,7 @@ export default function RCA() {
const handleExport = async (format: "md" | "pdf" | "docx") => {
if (!doc) return;
try {
await exportDocumentCmd(doc.id, format, ".");
await exportDocumentCmd(doc.id, doc.title, content, format, ".");
} catch (err) {
setError(String(err));
}

View File

@ -117,9 +117,9 @@ export default function Resolution() {
</span>
{step.done && <span className="text-xs text-primary font-medium">Resolved</span>}
</div>
<p className="text-sm font-medium">{step.why_question}</p>
{step.answer && <p className="text-sm text-muted-foreground mt-1">{step.answer}</p>}
{step.evidence && <p className="text-xs text-muted-foreground mt-1 italic">{step.evidence}</p>}
<p className="text-sm font-medium text-foreground">{step.why_question}</p>
{step.answer && <p className="text-sm text-foreground/80 mt-1">{step.answer}</p>}
{step.evidence && <p className="text-xs text-foreground/70 mt-1 italic">{step.evidence}</p>}
</div>
</div>
</div>

View File

@ -26,6 +26,7 @@ export default function Security() {
Object.fromEntries(piiPatterns.map((p) => [p.id, true]))
);
const [auditEntries, setAuditEntries] = useState<AuditEntry[]>([]);
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -49,6 +50,18 @@ export default function Security() {
setEnabledPatterns((prev) => ({ ...prev, [id]: !prev[id] }));
};
const toggleRow = (entryId: string) => {
setExpandedRows((prev) => {
const newSet = new Set(prev);
if (newSet.has(entryId)) {
newSet.delete(entryId);
} else {
newSet.add(entryId);
}
return newSet;
});
};
return (
<div className="p-6 space-y-6">
<div>
@ -133,35 +146,63 @@ export default function Security() {
<th className="text-left text-xs font-medium text-muted-foreground px-3 py-2">
Date
</th>
<th className="text-center text-xs font-medium text-muted-foreground px-3 py-2">
Details
</th>
</tr>
</thead>
<tbody>
{auditEntries.map((entry) => (
<tr key={entry.id} className="border-b last:border-0">
<td className="px-3 py-2 text-sm">
<Badge variant="outline">{entry.action}</Badge>
</td>
<td className="px-3 py-2 text-sm text-muted-foreground">
{entry.entity_id}
</td>
<td className="px-3 py-2">
<Badge
variant={
entry.details.includes("success")
? "default"
: entry.action === "blocked"
? "destructive"
: "secondary"
}
>
{entry.action}
</Badge>
</td>
<td className="px-3 py-2 text-xs text-muted-foreground">
{new Date(entry.timestamp).toLocaleString()}
</td>
</tr>
))}
{auditEntries.map((entry) => {
const isExpanded = expandedRows.has(entry.id);
return (
<React.Fragment key={entry.id}>
<tr className="border-b hover:bg-accent/50">
<td className="px-3 py-2 text-sm">
<Badge variant="outline">{entry.action}</Badge>
</td>
<td className="px-3 py-2 text-sm text-muted-foreground">
{entry.entity_id}
</td>
<td className="px-3 py-2">
<Badge
variant={
entry.details.includes("success")
? "default"
: entry.action === "blocked"
? "destructive"
: "secondary"
}
>
{entry.action}
</Badge>
</td>
<td className="px-3 py-2 text-xs text-muted-foreground">
{new Date(entry.timestamp).toLocaleString()}
</td>
<td className="px-3 py-2 text-center">
<button
onClick={() => toggleRow(entry.id)}
className="text-xs text-primary hover:underline"
>
{isExpanded ? "Hide" : "View"}
</button>
</td>
</tr>
{isExpanded && (
<tr className="border-b bg-accent/20">
<td colSpan={5} className="px-3 py-3">
<div className="text-xs">
<p className="font-medium text-foreground mb-1">Transmitted Data:</p>
<pre className="bg-background/50 p-2 rounded text-xs overflow-x-auto text-foreground/80">
{entry.details}
</pre>
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>

View File

@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import Security from "@/pages/Settings/Security";
import * as tauriCommands from "@/lib/tauriCommands";
vi.mock("@/lib/tauriCommands");
const mockAuditEntries: tauriCommands.AuditEntry[] = [
{
id: "audit-1",
timestamp: "2026-04-02T10:00:00Z",
action: "generate_rca",
entity_type: "document",
entity_id: "doc-123",
user_id: "user-1",
details: JSON.stringify({
issue_id: "issue-456",
content_hash: "abc123",
data: "Sample RCA content"
}),
},
{
id: "audit-2",
timestamp: "2026-04-02T11:00:00Z",
action: "ai_chat",
entity_type: "conversation",
entity_id: "conv-789",
user_id: "user-1",
details: JSON.stringify({
message: "What caused the issue?",
response_hash: "def456"
}),
},
];
describe("Audit Log", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(tauriCommands.getAuditLogCmd).mockResolvedValue(mockAuditEntries);
});
it("displays audit entries", async () => {
render(<Security />);
// Wait for audit log to load
await screen.findByText("Audit Log");
// Check that the table has rows (header + data rows)
const table = screen.getByRole("table");
expect(table).toBeInTheDocument();
const rows = screen.getAllByRole("row");
expect(rows.length).toBeGreaterThan(1); // At least header + 1 data row
});
it("provides way to view transmitted data details", async () => {
render(<Security />);
await screen.findByText("Audit Log");
// Should have View/Hide buttons for expanding details
const viewButtons = await screen.findAllByRole("button", { name: /View/i });
expect(viewButtons.length).toBeGreaterThan(0);
});
it("details column or button exists for viewing data", async () => {
render(<Security />);
await screen.findByText("Audit Log");
// The audit log should have a Details column header
const detailsHeader = screen.getByText("Details");
expect(detailsHeader).toBeInTheDocument();
// Should have view buttons
const viewButtons = await screen.findAllByRole("button", { name: /View/i });
expect(viewButtons.length).toBe(2); // One for each mock entry
});
});

View File

@ -0,0 +1,46 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import Dashboard from "@/pages/Dashboard";
import { useHistoryStore } from "@/stores/historyStore";
vi.mock("@/stores/historyStore");
describe("Dashboard Page", () => {
beforeEach(() => {
vi.mocked(useHistoryStore).mockReturnValue({
issues: [],
isLoading: false,
error: null,
searchQuery: "",
loadIssues: vi.fn(),
searchIssues: vi.fn(),
setSearchQuery: vi.fn(),
});
});
it("refresh button is visible with proper contrast", () => {
render(
<MemoryRouter>
<Dashboard />
</MemoryRouter>
);
const refreshButton = screen.getByRole("button", { name: /refresh/i });
expect(refreshButton).toBeInTheDocument();
// Button should have outline variant for visibility
expect(refreshButton.className).toContain("outline");
});
it("refresh button shows icon and text", () => {
render(
<MemoryRouter>
<Dashboard />
</MemoryRouter>
);
const refreshButton = screen.getByRole("button", { name: /refresh/i });
expect(refreshButton.textContent).toContain("Refresh");
});
});

View File

@ -0,0 +1,54 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { DocEditor } from "@/components/DocEditor";
describe("DocEditor Component", () => {
it("renders export buttons with readable text", () => {
const mockOnChange = vi.fn();
const mockOnExport = vi.fn();
render(
<DocEditor
content="# Test Content"
onChange={mockOnChange}
onExport={mockOnExport}
/>
);
const mdButton = screen.getByRole("button", { name: /MD/i });
const pdfButton = screen.getByRole("button", { name: /PDF/i });
const docxButton = screen.getByRole("button", { name: /DOCX/i });
expect(mdButton).toBeInTheDocument();
expect(pdfButton).toBeInTheDocument();
expect(docxButton).toBeInTheDocument();
// Buttons should have proper variant for visibility
expect(mdButton.className).toContain("outline");
expect(pdfButton.className).toContain("outline");
expect(docxButton.className).toContain("outline");
});
it("preview mode shows readable text", () => {
const mockOnChange = vi.fn();
render(
<DocEditor
content="# Test Heading\n\nTest content"
onChange={mockOnChange}
/>
);
// Switch to preview mode
const previewButton = screen.getByRole("button", { name: /Preview/i });
fireEvent.click(previewButton);
// Preview container should have prose classes for proper contrast
const previewContainers = document.querySelectorAll(".prose");
expect(previewContainers.length).toBeGreaterThan(0);
const mainContainer = previewContainers[0];
expect(mainContainer.className).toContain("text-foreground");
expect(mainContainer.className).toContain("dark:prose-invert");
});
});

View File

@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { invoke } from "@tauri-apps/api/core";
import { exportDocumentCmd } from "@/lib/tauriCommands";
vi.mock("@tauri-apps/api/core");
describe("exportDocumentCmd", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("includes title parameter when calling backend", async () => {
const mockInvoke = vi.mocked(invoke);
mockInvoke.mockResolvedValue("/path/to/doc.pdf");
const docId = "doc-123";
const title = "Test Document";
const contentMd = "# Test Content";
const format = "pdf";
const outputDir = ".";
await exportDocumentCmd(docId, title, contentMd, format, outputDir);
// Check that invoke was called with the correct parameters
expect(mockInvoke).toHaveBeenCalledWith(
"export_document",
expect.objectContaining({
title,
contentMd,
format,
outputDir,
})
);
});
it("handles missing title gracefully", async () => {
const mockInvoke = vi.mocked(invoke);
mockInvoke.mockRejectedValue("missing required key title");
const docId = "doc-123";
const title = "";
const contentMd = "# Test";
const format = "pdf";
const outputDir = ".";
await expect(exportDocumentCmd(docId, title, contentMd, format, outputDir)).rejects.toMatch(/title/i);
});
});

View File

@ -0,0 +1,98 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import History from "@/pages/History";
import { useHistoryStore } from "@/stores/historyStore";
import type { IssueSummary } from "@/lib/tauriCommands";
vi.mock("@/stores/historyStore");
const mockIssues: IssueSummary[] = [
{
id: "issue-1",
title: "Test Issue 1",
severity: "P3",
status: "open",
category: "linux",
created_at: "2026-01-01",
updated_at: "2026-01-01",
log_count: 1,
step_count: 2,
},
{
id: "issue-2",
title: "Test Issue 2",
severity: "P1",
status: "resolved",
category: "kubernetes",
created_at: "2026-01-02",
updated_at: "2026-01-02",
log_count: 0,
step_count: 3,
},
];
describe("History Page", () => {
beforeEach(() => {
vi.mocked(useHistoryStore).mockReturnValue({
issues: mockIssues,
isLoading: false,
error: null,
searchQuery: "",
loadIssues: vi.fn(),
searchIssues: vi.fn(),
setSearchQuery: vi.fn(),
});
});
it("displays domain column in table", () => {
render(
<MemoryRouter>
<History />
</MemoryRouter>
);
// Check that Domain header exists
const domainHeader = screen.getByText("Domain");
expect(domainHeader).toBeInTheDocument();
// Check that domain values are displayed
expect(screen.getByText("linux")).toBeInTheDocument();
expect(screen.getByText("kubernetes")).toBeInTheDocument();
});
it("domain filter is functional", () => {
const mockLoadIssues = vi.fn();
vi.mocked(useHistoryStore).mockReturnValue({
issues: mockIssues,
isLoading: false,
error: null,
searchQuery: "",
loadIssues: mockLoadIssues,
searchIssues: vi.fn(),
setSearchQuery: vi.fn(),
});
render(
<MemoryRouter>
<History />
</MemoryRouter>
);
// Find domain filter dropdown button
const domainFilter = screen.getByRole("button", { name: /All Domains/i });
expect(domainFilter).toBeInTheDocument();
});
it("search button is visible and readable", () => {
render(
<MemoryRouter>
<History />
</MemoryRouter>
);
const searchButton = screen.getByRole("button", { name: /search/i });
expect(searchButton).toBeInTheDocument();
expect(searchButton.className).toContain("outline");
});
});

View File

@ -0,0 +1,71 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import Resolution from "@/pages/Resolution";
import * as tauriCommands from "@/lib/tauriCommands";
vi.mock("@/lib/tauriCommands");
const mockIssueDetail = {
issue: {
id: "test-id",
title: "Test Issue",
description: "Test Description",
severity: "P3",
status: "open",
category: "linux",
source: "manual",
created_at: "2026-01-01",
updated_at: "2026-01-01",
assigned_to: "",
tags: "[]",
},
log_files: [],
resolution_steps: [
{
id: "step-1",
issue_id: "test-id",
step_order: 1,
why_question: "Why did the service crash?",
answer: "Out of memory",
evidence: "dmesg shows OOM killer",
created_at: "2026-01-01",
},
],
conversations: [],
};
describe("Resolution Page", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(tauriCommands.getIssueCmd).mockResolvedValue(mockIssueDetail);
});
it("renders resolution steps with readable text", async () => {
render(
<MemoryRouter initialEntries={["/issue/test-id/resolution"]}>
<Routes>
<Route path="/issue/:id/resolution" element={<Resolution />} />
</Routes>
</MemoryRouter>
);
const question = await screen.findByText("Why did the service crash?");
const answer = await screen.findByText("Out of memory");
const evidence = await screen.findByText("dmesg shows OOM killer");
// Check that text has proper contrast classes
expect(question).toBeInTheDocument();
expect(answer).toBeInTheDocument();
expect(evidence).toBeInTheDocument();
// Check for readable color classes (not muted-foreground which is too dark)
const questionParent = question.closest("p");
const answerParent = answer.closest("p");
const evidenceParent = evidence.closest("p");
expect(questionParent?.className).toContain("text-foreground");
expect(answerParent?.className).toMatch(/text-foreground/);
expect(evidenceParent?.className).toMatch(/text-foreground/);
});
});

View File

@ -1,4 +1,5 @@
import { vi, beforeAll, afterAll } from "vitest";
import "@testing-library/jest-dom/vitest";
// Mock Tauri core API
vi.mock("@tauri-apps/api/core", () => ({