Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions scripts/run-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -644,9 +644,9 @@ export const SUITES = {
"src/components/familiar-glyph-picker-panel.test.ts",
"src/components/familiar-glyph-loading.test.ts",
"src/components/familiar-studio-brain-tab.test.ts",
"src/components/familiar-studio-journal-tab.test.ts",
"src/components/journal-redirect.test.ts",
"src/components/familiar-studio-identity-tab.test.ts",
"src/components/familiar-studio-lifecycle-tab.test.ts",
"src/components/familiar-lifecycle-section.test.ts",
"src/components/github-task-field.test.ts",
"src/components/asana-task-field.test.ts",
"src/lib/task-asana.test.ts",
Expand Down
10 changes: 5 additions & 5 deletions src/components/delete-undo-surfaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ for (const rel of [
"./vault-panel.tsx",
"./automations-view.tsx",
"./journal/journal-entries.tsx",
"./familiar-studio-lifecycle-tab.tsx",
"./familiar-lifecycle-section.tsx",
]) {
const src = read(rel);
assert.match(src, /import \{ useUndoDelete \} from "@\/lib\/use-undo-delete"/, `${rel} imports useUndoDelete`);
Expand Down Expand Up @@ -41,11 +41,11 @@ for (const rel of [
assert.match(src, /day\?\.date !== deletePending\?\.item/, "journal treats the pending day as empty during the undo window");
}

// Familiar lifecycle: a familiar pending removal hides from BOTH the active and
// archived lists during the undo window (the toast is its only handle).
// Familiar lifecycle: a familiar pending removal loses its archive/remove
// controls during the undo window (the toast is its only handle).
{
const src = read("./familiar-studio-lifecycle-tab.tsx");
assert.match(src, /f\.id === pendingRemoveId/, "lifecycle tab hides the pending familiar row during the undo window");
const src = read("./familiar-lifecycle-section.tsx");
assert.match(src, /familiar\.id === pendingRemoveId/, "lifecycle section hides the pending familiar's controls during the undo window");
}

console.log("delete-undo-surfaces.test.ts OK");
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,22 @@ import assert from "node:assert/strict";
import { readFileSync } from "node:fs";

const source = readFileSync(
new URL("./familiar-studio-lifecycle-tab.tsx", import.meta.url),
new URL("./familiar-lifecycle-section.tsx", import.meta.url),
"utf8",
);

assert.match(source, /export function FamiliarStudioLifecycleTab/);
assert.match(source, /export function FamiliarLifecycleSection/);
assert.match(source, /archiveFamiliar/);
assert.match(source, /unarchiveFamiliar/);
assert.match(source, /clearAllFamiliarOverrides/);
assert.match(source, /clearGlyphOverride/);
assert.match(source, /clearFamiliarImage/);
// The roster (Active/Archived reorder + archive) renders here unconditionally —
// it's the manager the standalone "Manage familiars" page used to host, now part
// of Settings → Familiars (no more listView gate).
assert.match(source, /<h3 className="familiar-studio-lifecycle__heading">Active<\/h3>/, "the Lifecycle tab shows the active roster");
assert.match(source, /Archived/, "and the archived roster");
assert.match(source, /setFamiliarOrder/, "reordering the roster persists familiar order");
assert.match(source, /canMoveUp/, "Rows expose canMoveUp prop for disabled-edge state");
assert.match(source, /canMoveDown/, "Rows expose canMoveDown prop for disabled-edge state");
assert.match(source, /ph:arrow-up-bold/, "Move-up icon is wired");
assert.match(source, /ph:arrow-down-bold/, "Move-down icon is wired");
// The section is scoped to the SELECTED familiar (it lives at the bottom of the
// Identity tab) — the roster manager (reorder/list) retired with the Lifecycle
// tab, so no roster lists or ordering machinery may creep back in here.
assert.doesNotMatch(source, /setFamiliarOrder|DndContext|SortableContext|useSortable/, "no roster reordering in the lifecycle section");
assert.match(source, /isArchived \? \(/, "archive/unarchive toggles off the selected familiar's archived state");

// The active roster is also drag-reorderable (dnd-kit), alongside the arrows.
assert.match(source, /DndContext/, "active roster is wrapped in a DndContext");
assert.match(source, /SortableContext/, "active rows are sortable");
assert.match(source, /useSortable/, "each active row hooks the sortable transform");
assert.match(source, /ph:dots-six-vertical/, "each active row exposes a drag grip");
assert.match(source, /arrayMove\(ids, oldIndex, newIndex\)/, "drag reorder moves the id then persists via setFamiliarOrder");

// The roster order here is distinct from the avatar-strip pin order in
// Appearance — the hint cross-links so users find both (2026-07-06).
assert.match(source, /avatar strip(?:'|&apos;)s pinned order/, "lifecycle hint disambiguates roster order from pin order");
assert.match(source, /window\.location\.hash = "appearance"/, "lifecycle hint links to Appearance");
// The roster order hint moved out with the reorder UI; archive-vs-remove
// semantics still explained in-product.
assert.match(source, /Archive hides a familiar/, "archive-vs-remove semantics are explained in-product");

// ── Dual-track lifecycle (cave-ykwk): Remove = undo-safe detach ─────────────
// Archive stays the reversible hide; Remove detaches a mistaken binding. These
Expand All @@ -51,16 +35,16 @@ assert.match(
"remove commits as DELETE /api/familiars/[id]",
);

// Every row (active AND archived) exposes Remove as a distinct action beside
// Archive/Unarchive, with an inline confirm strip that spells out detach
// semantics: what is cleared vs. what survives, plus the active-session warning.
// Remove is a distinct action beside Archive/Unarchive, with an inline confirm
// strip that spells out detach semantics: what is cleared vs. what survives,
// plus the active-session warning.
assert.match(source, /aria-label=\{`Remove \$\{familiar\.display_name\}`\}/);
assert.match(source, /aria-label=\{`Archive \$\{familiar\.display_name\}`\}/);
assert.match(source, /aria-label=\{`Unarchive \$\{familiar\.display_name\}`\}/);
assert.match(source, /roster entry and agent binding/, "confirm copy explains what is cleared");
assert.match(source, /stay on\s+your disk/, "confirm copy explains what survives");
assert.match(source, /active_sessions/, "confirm warns using the daemon session count");
assert.match(source, /keep\s+running until they finish/);
assert.match(source, /Archive hides a familiar/, "archive-vs-remove semantics are explained in-product");

// Restore path: Recently removed shelf + POST restore + announcer feedback,
// then a roster re-fetch threaded from Settings.
Expand All @@ -81,6 +65,19 @@ assert.match(source, /onRosterChanged\?\.\(\)/);
assert.doesNotMatch(removeFlow, /archiveFamiliar|unarchiveFamiliar/, "remove leaves the archive store alone");
}

// The section renders from the Identity tab — the only lifecycle home now.
{
const identityTab = readFileSync(
new URL("./familiar-studio-identity-tab.tsx", import.meta.url),
"utf8",
);
assert.match(
identityTab,
/<FamiliarLifecycleSection familiar=\{familiar\} onRosterChanged=\{onRosterChanged\} \/>/,
"the Identity tab hosts the lifecycle section",
);
}

// ── Route seams ──────────────────────────────────────────────────────────────
{
const path = await import("node:path");
Expand Down Expand Up @@ -126,4 +123,4 @@ assert.match(source, /onRosterChanged\?\.\(\)/);
assert.match(removedRoute, /status: 409/);
}

console.log("familiar-studio-lifecycle-tab.test.ts: ok");
console.log("familiar-lifecycle-section.test.ts: ok");
Loading
Loading