From 6bcf98a83667dec1c2e34fb109d09b8036735825 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 16 Sep 2026 13:57:03 +0900 Subject: [PATCH 1/2] test(claude-sdk-oauth): pin the provider-excursion contract shipped in #1749 --- ...de-sdk-oauth-binding-lifecycle-security.test.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 6a2850f965..0b960361b6 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,12 @@ describe("Claude SDK OAuth binding lifecycle security", () => { context(sessionId, [], sessionFile), ); - expect(getBinding(sessionId)).toBeUndefined(); - expect(extension.persisted).toContainEqual({ + expect(getBinding(sessionId)?.sdkSessionId).toBe("sdk-before-switch"); + 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 () => { From 487d3affab82a50073cdfa7fa10a983f6439a9ca Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Wed, 16 Sep 2026 14:09:33 +0900 Subject: [PATCH 2/2] test(claude-sdk-oauth): assert the kept binding by presence, not the injected id keepBindingThenClose re-remembers the binding from the live session entry, so it carries the entry sdkSessionId rather than the value the test injected. The security property is that durable state survives the excursion; the reattach prefix is pinned by the #1747 regression suite. --- .../claude-sdk-oauth-binding-lifecycle-security.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 0b960361b6..25c4c4a9b1 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 @@ -203,7 +203,11 @@ describe("Claude SDK OAuth binding lifecycle security", () => { context(sessionId, [], sessionFile), ); - expect(getBinding(sessionId)?.sdkSessionId).toBe("sdk-before-switch"); + // 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" },