setup-agent 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/host.test.ts CHANGED
@@ -163,6 +163,70 @@ describe("CheckoutHost", () => {
163
163
  });
164
164
 
165
165
  describe("plans", () => {
166
+ it.each([
167
+ { decision: "approve" },
168
+ { decision: "revise", feedback: "Use pnpm" },
169
+ ] satisfies Checkout.PlanDecision[])(
170
+ "rejects a $decision decision for a superseded revision",
171
+ async (decision) => {
172
+ const host = mount();
173
+ await host.commands["checkout/create"](seed);
174
+ await host.commands["checkout/begin-plan"]();
175
+ await host.commands["agent/plan"]({ markdown: "Install chat" });
176
+ const pending = { ...decision, id: seed.id, revision: 1 };
177
+ await host.commands["agent/plan"]({ markdown: "Replace the API route" });
178
+
179
+ expect(await reason(host.commands["checkout/plan"](pending))).toEqual({
180
+ reason: "stale-plan",
181
+ });
182
+ expect(currentPlan(host.state)).toMatchObject({
183
+ revision: 2,
184
+ status: "proposed",
185
+ });
186
+ expect(host.state.status).toBe("planning");
187
+ expect(currentPlan(host.state)?.feedback).toBeUndefined();
188
+
189
+ await host.commands["checkout/plan"]({ ...pending, revision: 2 });
190
+ expect(currentPlan(host.state)?.status).toBe(
191
+ decision.decision === "approve" ? "approved" : "changes-requested",
192
+ );
193
+ },
194
+ );
195
+
196
+ it("rejects a decision from an earlier setup with the same plan revision", async () => {
197
+ const host = mount();
198
+ await host.commands["checkout/create"](seed);
199
+ await host.commands["checkout/begin-plan"]();
200
+ await host.commands["agent/plan"]({ markdown: "Install chat" });
201
+ await host.commands["checkout/cancel"]({ id: seed.id });
202
+ await host.commands["checkout/create"]({ ...seed, id: "replacement" });
203
+ await host.commands["checkout/begin-plan"]();
204
+ await host.commands["agent/plan"]({ markdown: "Install cloud" });
205
+
206
+ expect(
207
+ await reason(
208
+ host.commands["checkout/plan"]({
209
+ decision: "approve",
210
+ id: seed.id,
211
+ revision: 1,
212
+ }),
213
+ ),
214
+ ).toEqual({ reason: "stale-plan" });
215
+ expect(currentPlan(host.state)).toMatchObject({
216
+ revision: 1,
217
+ status: "proposed",
218
+ });
219
+ expect(host.state.status).toBe("planning");
220
+
221
+ await host.commands["checkout/plan"]({
222
+ decision: "approve",
223
+ id: "replacement",
224
+ revision: 1,
225
+ });
226
+ expect(currentPlan(host.state)?.status).toBe("approved");
227
+ expect(host.state.status).toBe("installing");
228
+ });
229
+
166
230
  it("refuses steps until the user approves a plan", async () => {
167
231
  const host = mount();
168
232
  await host.commands["checkout/create"](seed);
@@ -606,6 +670,46 @@ describe("inputs", () => {
606
670
  describe("model inputs", () => {
607
671
  const answer = { provider: "openai", model: "gpt-test" };
608
672
 
673
+ it("stores a valid redeem hash only on model inputs", async () => {
674
+ const host = mount();
675
+ await host.commands["checkout/create"](seed);
676
+ await host.commands["checkout/begin-plan"]();
677
+ const hash = "a".repeat(64);
678
+ const { inputId } = await host.commands["agent/ask"]({
679
+ ...presetInput("llm-provider"),
680
+ redeemKeyHash: hash,
681
+ });
682
+ expect(
683
+ host.state.inputs.find((input) => input.id === inputId)?.redeemKeyHash,
684
+ ).toBe(hash);
685
+ await host.commands["checkout/answer"]({
686
+ inputId,
687
+ answer: JSON.stringify(answer),
688
+ });
689
+ expect(
690
+ host.state.inputs.find((input) => input.id === inputId)?.redeemKeyHash,
691
+ ).toBe(hash);
692
+ for (const redeemKeyHash of [
693
+ "A".repeat(64),
694
+ "a".repeat(63),
695
+ "not-a-hash",
696
+ ]) {
697
+ expect(
698
+ await reason(
699
+ host.commands["agent/ask"]({
700
+ ...presetInput("llm-provider"),
701
+ redeemKeyHash,
702
+ }),
703
+ ),
704
+ ).toEqual({ reason: "invalid-input" });
705
+ }
706
+ expect(
707
+ await reason(
708
+ host.commands["agent/ask"]({ prompt: "Name?", redeemKeyHash: hash }),
709
+ ),
710
+ ).toEqual({ reason: "invalid-input" });
711
+ });
712
+
609
713
  it("parses a model answer and rejects malformed ones", () => {
610
714
  expect(parseModelAnswer(JSON.stringify(answer))).toEqual(answer);
611
715
  expect(
package/src/host.ts CHANGED
@@ -86,6 +86,12 @@ const useCheckoutHost = (restored: unknown) => {
86
86
 
87
87
  const ask = (seed: Checkout.InputSeed & { stepId?: string }) => {
88
88
  const kind = seed.kind ?? "text";
89
+ if (
90
+ seed.redeemKeyHash !== undefined &&
91
+ (kind !== "model" || !/^[0-9a-f]{64}$/.test(seed.redeemKeyHash))
92
+ ) {
93
+ throw reject("invalid-input", "invalid model redeem key hash");
94
+ }
89
95
  const options =
90
96
  kind === "entry-point"
91
97
  ? parseEntryPointOptions(seed.options)
@@ -166,6 +172,9 @@ const useCheckoutHost = (restored: unknown) => {
166
172
  ...(kind === "product" && { product: seed.product }),
167
173
  ...(seed.default !== undefined && { default: seed.default }),
168
174
  ...(seed.help !== undefined && { help: seed.help }),
175
+ ...(seed.redeemKeyHash !== undefined && {
176
+ redeemKeyHash: seed.redeemKeyHash,
177
+ }),
169
178
  optional: seed.optional ?? false,
170
179
  status: "open",
171
180
  ...(stepId !== undefined && { stepId }),
@@ -331,6 +340,15 @@ const useCheckoutHost = (restored: unknown) => {
331
340
  if (plan === undefined) {
332
341
  throw reject("no-plan", "the agent has not proposed a plan yet");
333
342
  }
343
+ if (
344
+ (params.id !== undefined && params.id !== state.id) ||
345
+ (params.revision !== undefined && params.revision !== plan.revision)
346
+ ) {
347
+ throw reject(
348
+ "stale-plan",
349
+ "review the current setup plan before deciding",
350
+ );
351
+ }
334
352
  if (plan.status !== "proposed") {
335
353
  throw reject("plan-decided", `the plan is already ${plan.status}`);
336
354
  }
package/src/protocol.ts CHANGED
@@ -101,6 +101,7 @@ export namespace Checkout {
101
101
  product?: string;
102
102
  default?: string;
103
103
  help?: InputHelp;
104
+ redeemKeyHash?: string;
104
105
  optional: boolean;
105
106
  status: InputStatus;
106
107
  answer?: string;
@@ -192,12 +193,16 @@ export namespace Checkout {
192
193
  product?: string;
193
194
  default?: string;
194
195
  help?: InputHelp;
196
+ redeemKeyHash?: string;
195
197
  optional?: boolean;
196
198
  };
197
199
 
198
- export type PlanDecision =
199
- | { decision: "approve" }
200
- | { decision: "revise"; feedback: string };
200
+ export type PlanDecision = {
201
+ /** The setup the browser reviewed; omitted by legacy clients. */
202
+ id?: string;
203
+ /** The plan revision the browser reviewed; omitted by legacy clients. */
204
+ revision?: number;
205
+ } & ({ decision: "approve" } | { decision: "revise"; feedback: string });
201
206
 
202
207
  export type Commands = {
203
208
  "checkout/create": (params: {
@@ -254,6 +259,7 @@ export namespace Checkout {
254
259
  | "plan-required"
255
260
  | "no-plan"
256
261
  | "plan-decided"
262
+ | "stale-plan"
257
263
  | "finish-not-proposed"
258
264
  | "feedback-required"
259
265
  | "empty-plan"