singularity-forge/web/lib/__tests__/onboarding-logic.test.ts
Mikael Hugo 0b5fa75c0d fix(lint): fix all pre-existing lint failures
- check-sf-extension-inventory.mjs: expand parseDirectRegisteredCommands()
  scan to include 7 more files (guards/inturn.js, notifications/notify.js,
  permissions/index.js, ui/usage-bar.js, commands/legacy/audit.js,
  commands/legacy/create-extension.js, commands/legacy/create-slash-command.js)
  and filter results by BASE_RUNTIME_COMMAND_NAMES to exclude doc-string false
  positives ("name" in create-slash-command.js template text)

- extension-manifest.json: remove 'clear' (subcommand of logs/notifications,
  never a top-level pi.registerCommand)

- packages/pi-agent-core/src/db/sf-db.ts: fix 23 noVoidTypeReturn errors
  - openDatabase: void → boolean (caller uses return value at line 5625)
  - claimEscalationOverride: void → boolean (caller checks at escalation.js:243)
  - resolveSelfFeedbackEntry: void → boolean (caller checks at self-feedback.js:387)
  - copyWorktreeDb: void → boolean (caller checks at reconcileWorktreeDb)
  - compactUokMessages: void → {before,after} (caller returns value at message-bus.js:238)
  - insertSessionTurn: void → bigint|null (caller uses id at session-recorder.js:104)
  - expireStaleMemories: void → number (caller uses count at auto-start.js:1047)
  - deleteMemorySourceRow: void → boolean (caller returns value at memory-source-store.js:107)
  - deleteMemoryEmbedding: void → boolean (caller returns value at memory-embeddings.js:328)
  - updateBacklogItemStatus: remove dead return expression (callers discard value)
  - removeBacklogItem: remove dead return expression (callers discard value)
  - updateGateCircuitBreaker: remove dead return {total,avgMs,...} (wrong-type
    code accidentally merged from getGateLatencyStats, never reachable)
  - markUokMessageRead: remove dead return true/false (callers discard value)

- Auto-fix formatting and organizeImports in ~30 source files (biome --write)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 04:02:31 +02:00

250 lines
7.6 KiB
TypeScript

import assert from "node:assert/strict";
import { describe, test } from "vitest";
/**
* Tests for pure functions in the onboarding layer.
*
* These functions are extracted inline (most are module-private in
* onboarding-service.ts) and tested without DOM, subprocess, or I/O.
*
* See: src/web/onboarding-service.ts — resolveOnboardingLockReason,
* redactSensitiveText, extractErrorDetail, sanitizeMessage.
* web/app/api/onboarding/route.ts — isActionPayload.
*/
// ---------------------------------------------------------------------------
// Extracted pure functions (mirrors the originals — kept in sync by contract)
// ---------------------------------------------------------------------------
type OnboardingBridgeAuthRefreshPhase =
| "idle"
| "pending"
| "succeeded"
| "failed";
interface OnboardingBridgeAuthRefreshState {
phase: OnboardingBridgeAuthRefreshPhase;
strategy: "restart" | null;
startedAt: string | null;
completedAt: string | null;
error: string | null;
}
type OnboardingLockReason =
| "required_setup"
| "bridge_refresh_pending"
| "bridge_refresh_failed";
/** Mirrors resolveOnboardingLockReason from onboarding-service.ts */
function resolveOnboardingLockReason(
requiredSatisfied: boolean,
bridgeAuthRefresh: OnboardingBridgeAuthRefreshState,
): OnboardingLockReason | null {
if (!requiredSatisfied) {
return "required_setup";
}
if (bridgeAuthRefresh.phase === "pending") {
return "bridge_refresh_pending";
}
if (bridgeAuthRefresh.phase === "failed") {
return "bridge_refresh_failed";
}
return null;
}
/** Mirrors redactSensitiveText from onboarding-service.ts */
function redactSensitiveText(value: string): string {
return value
.replace(/sk-[A-Za-z0-9_-]{6,}/g, "[redacted]")
.replace(/xox[baprs]-[A-Za-z0-9-]+/g, "[redacted]")
.replace(/Bearer\s+[^\s]+/gi, "Bearer [redacted]")
.replace(
/([A-Z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET)["'=:\s]+)([^\s,;"']+)/gi,
"$1[redacted]",
);
}
/** Mirrors extractErrorDetail from onboarding-service.ts */
function extractErrorDetail(payload: unknown): string | null {
if (!payload) return null;
if (typeof payload === "string") return payload;
if (typeof payload !== "object") return null;
const record = payload as Record<string, unknown>;
const candidates = [
record.message,
record.error,
record.detail,
record.error_description,
];
for (const candidate of candidates) {
if (typeof candidate === "string" && candidate.trim().length > 0) {
return candidate;
}
const nested = extractErrorDetail(candidate);
if (nested) return nested;
}
return null;
}
/** Mirrors isActionPayload from web/app/api/onboarding/route.ts */
function isActionPayload(value: unknown): boolean {
return (
typeof value === "object" &&
value !== null &&
typeof (value as { action?: unknown }).action === "string"
);
}
function idleBridgeRefresh(): OnboardingBridgeAuthRefreshState {
return {
phase: "idle",
strategy: null,
startedAt: null,
completedAt: null,
error: null,
};
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("resolveOnboardingLockReason", () => {
test("lockReason_whenRequiredNotSatisfied_returnsRequiredSetup", () => {
const result = resolveOnboardingLockReason(false, idleBridgeRefresh());
assert.equal(result, "required_setup");
});
test("lockReason_whenRequiredSatisfiedAndRefreshPending_returnsBridgeRefreshPending", () => {
const refresh: OnboardingBridgeAuthRefreshState = {
...idleBridgeRefresh(),
phase: "pending",
};
const result = resolveOnboardingLockReason(true, refresh);
assert.equal(result, "bridge_refresh_pending");
});
test("lockReason_whenRequiredSatisfiedAndRefreshFailed_returnsBridgeRefreshFailed", () => {
const refresh: OnboardingBridgeAuthRefreshState = {
...idleBridgeRefresh(),
phase: "failed",
error: "connection refused",
};
const result = resolveOnboardingLockReason(true, refresh);
assert.equal(result, "bridge_refresh_failed");
});
test("lockReason_whenRequiredSatisfiedAndRefreshIdle_returnsNull", () => {
const result = resolveOnboardingLockReason(true, idleBridgeRefresh());
assert.equal(result, null);
});
test("lockReason_whenRequiredSatisfiedAndRefreshSucceeded_returnsNull", () => {
const refresh: OnboardingBridgeAuthRefreshState = {
...idleBridgeRefresh(),
phase: "succeeded",
};
const result = resolveOnboardingLockReason(true, refresh);
assert.equal(result, null);
});
test("lockReason_requiredSetupTakesPrecedenceOverPendingRefresh", () => {
// Even if bridge refresh is pending, missing provider setup wins.
const refresh: OnboardingBridgeAuthRefreshState = {
...idleBridgeRefresh(),
phase: "pending",
};
const result = resolveOnboardingLockReason(false, refresh);
assert.equal(result, "required_setup");
});
});
describe("redactSensitiveText", () => {
test("redact_whenApiKeyInText_replacesWithPlaceholder", () => {
const result = redactSensitiveText("key: sk-abcdef1234567890");
assert.ok(
!result.includes("sk-abcdef"),
`expected redaction, got: ${result}`,
);
assert.ok(result.includes("[redacted]"));
});
test("redact_whenBearerToken_replacesWithPlaceholder", () => {
const result = redactSensitiveText(
"Authorization: Bearer eyJhbGciOiJSUzI1NiJ9",
);
assert.ok(result.includes("Bearer [redacted]"), `got: ${result}`);
});
test("redact_whenSlackToken_replacesWithPlaceholder", () => {
const result = redactSensitiveText("token=xoxb-abc123-def456");
assert.ok(!result.includes("xoxb-abc123"), `got: ${result}`);
assert.ok(result.includes("[redacted]"));
});
test("redact_whenNoSensitiveData_returnsUnchanged", () => {
const input = "validation failed: network timeout";
assert.equal(redactSensitiveText(input), input);
});
});
describe("extractErrorDetail", () => {
test("extractErrorDetail_whenString_returnsItself", () => {
assert.equal(
extractErrorDetail("something went wrong"),
"something went wrong",
);
});
test("extractErrorDetail_whenObjectWithMessage_returnsMessage", () => {
assert.equal(extractErrorDetail({ message: "auth failed" }), "auth failed");
});
test("extractErrorDetail_whenObjectWithError_returnsError", () => {
assert.equal(
extractErrorDetail({ error: "invalid token" }),
"invalid token",
);
});
test("extractErrorDetail_whenNull_returnsNull", () => {
assert.equal(extractErrorDetail(null), null);
});
test("extractErrorDetail_whenNestedMessage_returnsNestedValue", () => {
const payload = { wrapper: { message: "inner error" } };
// extractErrorDetail only recurses into known keys, not arbitrary keys.
// The 'wrapper' key is not in the candidate list, so this returns null.
assert.equal(extractErrorDetail(payload), null);
});
test("extractErrorDetail_whenObjectWithErrorDescription_returnsIt", () => {
assert.equal(
extractErrorDetail({ error_description: "token expired" }),
"token expired",
);
});
});
describe("isActionPayload", () => {
test("isActionPayload_whenObjectWithStringAction_returnsTrue", () => {
assert.equal(isActionPayload({ action: "recheck" }), true);
});
test("isActionPayload_whenNull_returnsFalse", () => {
assert.equal(isActionPayload(null), false);
});
test("isActionPayload_whenString_returnsFalse", () => {
assert.equal(isActionPayload("recheck"), false);
});
test("isActionPayload_whenObjectWithNumericAction_returnsFalse", () => {
assert.equal(isActionPayload({ action: 42 }), false);
});
test("isActionPayload_whenObjectMissingAction_returnsFalse", () => {
assert.equal(isActionPayload({ provider: "anthropic" }), false);
});
});