diff --git a/packages/coding-agent/test/claude-sdk-oauth-binding-lifecycle-security.test.ts b/packages/coding-agent/test/claude-sdk-oauth-binding-lifecycle-security.test.ts index 6a2850f96..25c4c4a9b 100644 --- a/packages/coding-agent/test/claude-sdk-oauth-binding-lifecycle-security.test.ts +++ b/packages/coding-agent/test/claude-sdk-oauth-binding-lifecycle-security.test.ts @@ -174,7 +174,13 @@ afterEach(() => { }); describe("Claude SDK OAuth binding lifecycle security", () => { - it("forgets process binding and invalidates/deletes durable state when selecting a non-Claude provider", async () => { + // A provider excursion is not an invalidation (senpi#1747): leaving this provider closes the + // live SDK query but keeps the binding, so returning to the same Claude model reattaches at the + // recorded prefix instead of re-sending the whole conversation. Durable state is still deleted + // for a genuine invalidation - the assistant-rewritten and extensions-removed cases below keep + // that half of the contract pinned - and a binding whose identity drifted is refused on the way + // back by identityDrift / withoutUnconfirmedResume rather than by deleting the sidecar here. + it("keeps the binding and its durable state when selecting a non-Claude provider", async () => { const sessionId = "model-select-non-claude"; const { sessionFile } = makeSession(); const extension = fakeExtension(); @@ -197,12 +203,16 @@ describe("Claude SDK OAuth binding lifecycle security", () => { context(sessionId, [], sessionFile), ); - expect(getBinding(sessionId)).toBeUndefined(); - expect(extension.persisted).toContainEqual({ + // The binding survives the excursion - keepBindingThenClose re-remembers it from the live + // session entry, so it carries the entry's own sdkSessionId. The security property here is that + // durable state is NOT destroyed; the reattach-at-the-recorded-prefix behavior is pinned + // separately by the #1747 regression suite. + expect(getBinding(sessionId)).toBeDefined(); + expect(extension.persisted).not.toContainEqual({ customType: BINDING_ENTRY_TYPE, data: { schemaVersion: 1, invalidated: true, reason: "model_selected" }, }); - expect(existsSync(bindingSidecarPath(sessionFile))).toBe(false); + expect(existsSync(bindingSidecarPath(sessionFile))).toBe(true); }); it("forgets process state and deletes durable state when an assistant message is rewritten", async () => {