fix(tests): stop the settings test destroying the configured API key
The test overwrote ai_api_key with a marker and then restored it to "" rather than to its previous value, so every run of the integration suite silently wiped the AI provider key. Symptom appears much later and looks nothing like a test problem: AI features quietly stop working. Captures the existing value first and restores it in a finally block, so a failed assertion cannot leave the marker in place either. Deletes the record instead when the test created it. The suite runs against the shared instance — there is no separate test database — so anything it touches, it must put back exactly. feat(board): give the owner both a quick box and Plan with AI The Phase 4 spec made the intake wizard client-facing only, reasoning that the developer creates cards directly. That was wrong about how he wants to work. The quick box stays the default for lines he already understands; Plan with AI runs the same interview for work that is genuinely unformed. Hidden entirely when no provider is configured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7059105d78
commit
8888acb2c6
3 changed files with 111 additions and 10 deletions
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { AddCard } from "@/components/board/add-card";
|
||||
import { OwnerAdd } from "@/components/intake/owner-add";
|
||||
import { BoardColumn } from "@/components/board/board-column";
|
||||
import { CardTile } from "@/components/board/card-tile";
|
||||
import { ClientCardControls } from "@/components/board/client-card-controls";
|
||||
|
|
@ -261,8 +261,8 @@ export function Board({
|
|||
))}
|
||||
</SortableContext>
|
||||
{role === "owner" && (
|
||||
<AddCard
|
||||
role={role}
|
||||
<OwnerAdd
|
||||
assistantAvailable={assistantAvailable}
|
||||
projectId={projectId}
|
||||
columnId={column.id}
|
||||
clientSlug={clientSlug}
|
||||
|
|
|
|||
81
src/components/intake/owner-add.tsx
Normal file
81
src/components/intake/owner-add.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { AddCard } from "@/components/board/add-card";
|
||||
import { IntakeWizard } from "@/components/intake/intake-wizard";
|
||||
|
||||
/**
|
||||
* The owner's two ways to add work.
|
||||
*
|
||||
* The quick box is the default and stays the default: most of what the
|
||||
* developer adds is a line they already understand — "fix the footer spacing" —
|
||||
* and sitting through an interview for that would make the fast path slow.
|
||||
*
|
||||
* "Plan with AI" runs the same interview a client gets, for the times the work
|
||||
* is genuinely unformed and worth thinking through. The Phase 4 spec said the
|
||||
* wizard was client-facing only; that was wrong about how the developer
|
||||
* actually wants to work, so both are offered here.
|
||||
*
|
||||
* The button is hidden entirely when no provider is configured — an AI button
|
||||
* that always fails is worse than no button.
|
||||
*/
|
||||
export function OwnerAdd({
|
||||
projectId,
|
||||
columnId,
|
||||
clientSlug,
|
||||
projectSlug,
|
||||
assistantAvailable,
|
||||
}: {
|
||||
projectId: string;
|
||||
columnId: string;
|
||||
clientSlug: string;
|
||||
projectSlug: string;
|
||||
assistantAvailable: boolean;
|
||||
}) {
|
||||
const [planning, setPlanning] = useState(false);
|
||||
|
||||
if (planning) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<IntakeWizard
|
||||
projectId={projectId}
|
||||
clientSlug={clientSlug}
|
||||
projectSlug={projectSlug}
|
||||
onFallback={() => setPlanning(false)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPlanning(false)}
|
||||
className="text-[11px] text-text-muted hover:text-text-secondary"
|
||||
>
|
||||
Cancel and go back
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<AddCard
|
||||
role="owner"
|
||||
projectId={projectId}
|
||||
columnId={columnId}
|
||||
clientSlug={clientSlug}
|
||||
projectSlug={projectSlug}
|
||||
/>
|
||||
|
||||
{assistantAvailable && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPlanning(true)}
|
||||
className="flex items-center gap-1.5 rounded-[var(--radius-tile)] px-2 py-1.5 text-xs font-medium text-text-muted transition-colors hover:bg-surface-raised hover:text-text-secondary"
|
||||
>
|
||||
<Sparkles size={13} strokeWidth={2} />
|
||||
Plan with AI
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -24,18 +24,38 @@ describe("the AI key never leaves the server", () => {
|
|||
.collection("settings")
|
||||
.getFirstListItem("id != ''")
|
||||
.catch(() => null);
|
||||
|
||||
// CAPTURE THE REAL KEY BEFORE OVERWRITING IT.
|
||||
//
|
||||
// An earlier version of this test restored to "" instead of to whatever
|
||||
// was there, so every run of the integration suite silently destroyed the
|
||||
// configured API key. It took a live debugging session to notice, because
|
||||
// the only symptom is that AI features quietly stop working later.
|
||||
//
|
||||
// This suite runs against the shared instance — there is no separate test
|
||||
// database — so anything it touches, it must put back exactly.
|
||||
const previousKey: string = existing?.ai_api_key ?? "";
|
||||
|
||||
const record = existing
|
||||
? await admin.collection("settings").update(existing.id, { ai_api_key: marker })
|
||||
: await admin.collection("settings").create({ ai_api_key: marker });
|
||||
|
||||
const cookie = await signIn(DEMO.ownerEmail, DEMO.ownerPassword);
|
||||
const response = await fetch(`${APP_URL}/admin/settings`, { headers: { cookie } });
|
||||
const html = await response.text();
|
||||
try {
|
||||
const cookie = await signIn(DEMO.ownerEmail, DEMO.ownerPassword);
|
||||
const response = await fetch(`${APP_URL}/admin/settings`, { headers: { cookie } });
|
||||
const html = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(html).not.toContain(marker);
|
||||
|
||||
await admin.collection("settings").update(record.id, { ai_api_key: "" });
|
||||
expect(response.status).toBe(200);
|
||||
expect(html).not.toContain(marker);
|
||||
} finally {
|
||||
// `finally`, so a failed assertion cannot leave the marker in place.
|
||||
if (existing) {
|
||||
await admin.collection("settings").update(record.id, { ai_api_key: previousKey });
|
||||
} else {
|
||||
// We created this record; remove it rather than leaving an empty one.
|
||||
await admin.collection("settings").delete(record.id).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue