mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 11:01:16 +01:00
A company with no agent cannot do anything: no runs, no tasks, nothing to show. The dashboard says so in a banner with a link, which asks the customer to notice a problem the product can fix for them. It is worse for a company created by Paperclip Cloud: Cloud creates the company before the tenant boots, so the companyless redirect never runs, and the customer arrives on an empty dashboard straight out of a signup flow that already asked for a mission. The dashboard now opens onboarding when the agent list has loaded and is empty, and onboarding opens on the agent step when the company already has its mission — read from the company-level goal the seed writes, under the query key the launch path already uses, so it shares a cache entry rather than adding a fetch. The step is decided once. `initialStep` is derived from the company list and the goal list, so it changes on any retry, refetch or cache invalidation. An effect that took it as a dependency called `setStep` on every one of those and moved a customer who was already mid-flow. Gating the input only narrowed that window; it could not close it. The step now belongs to the request that opened the wizard: the effect reads it through a ref and is keyed on the wizard opening or the company changing. `createdCompanyIdRef` beside it already used this pattern for the same reason. That exposed a path nothing had ever taken. A company reached the mission step only by creating itself on step 1, so opening an existing company there found code that had never run: `companyName` is only typed on step 1, and both ways forward require it, so the step could not be completed at all; and confirming advanced without writing anything, so the mission the customer typed was discarded. Both fixed, and the write now reconciles against the goal list rather than adding a second company goal, since the mission lookup fails open and can send a company that has one back to that step. Company-scoped state now stays with its company. `clearCompanyScopedState` runs when the route replaces a company and when it withdraws one — the same event, and clearing half of it left a goal id that made the next company skip a mission it had never given. `stillTheSameCompany` guards all five async writes, after the server work rather than before it, so a company switch mid-flight cannot hand the new company the old one's goal, project, issue or agent, and cannot leave a hired agent without its instructions file. The keyboard path honours `loading` like every button already did. `claimOnboardingOffer` makes onboarding an offer that stays declined for the visit. Route ownership is now recorded whenever the route names a company, including one the wizard already holds. This changes a documented rule deliberately: without it a self-created company was never withdrawn, so `/onboarding` would show "create a company" while still holding the previous one and write the customer's new mission into it. Tested at the seam, because every defect on this branch lived between a value and its consumer and the predicate tests passed at every stage. `OnboardingWizard.step.test.tsx` renders the real wizard against the real resolver and the real mission hook across 18 cases, and each was fault-injected against the code it replaces rather than trusted on a green run. That caught a case that passed against the broken code, and a race in one of the guards. ui typecheck clean; full ui suite 3923 pass, with one timezone-dependent IssueProperties failure present on this branch's base in a file this change does not touch. All CI gates green; Greptile 5/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
28 lines
1.1 KiB
TypeScript
28 lines
1.1 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import { claimOnboardingOffer, resetOnboardingOffers } from "./onboarding-auto-open";
|
|
|
|
describe("claimOnboardingOffer", () => {
|
|
beforeEach(resetOnboardingOffers);
|
|
|
|
it("lets the first offer through", () => {
|
|
expect(claimOnboardingOffer("company-1")).toBe(true);
|
|
});
|
|
|
|
it("refuses a second offer for the same company", () => {
|
|
// The dashboard effect re-runs whenever a query behind it refetches, and
|
|
// again on every return to the page. Neither may reopen a wizard the
|
|
// customer closed.
|
|
claimOnboardingOffer("company-1");
|
|
expect(claimOnboardingOffer("company-1")).toBe(false);
|
|
});
|
|
|
|
it("offers each company once, in any order", () => {
|
|
// A single stored company id fails here: declining for A, visiting B, then
|
|
// returning to A would offer A again.
|
|
expect(claimOnboardingOffer("company-a")).toBe(true);
|
|
expect(claimOnboardingOffer("company-b")).toBe(true);
|
|
expect(claimOnboardingOffer("company-a")).toBe(false);
|
|
expect(claimOnboardingOffer("company-b")).toBe(false);
|
|
});
|
|
});
|