| ... | @@ -496,9 +496,10 @@ describe("WebSocket Server", () => { | ... | @@ -496,9 +496,10 @@ describe("WebSocket Server", () => { |
| 496 | }); | 496 | }); |
| 497 | | 497 | |
| 498 | it("responds to server.getConfig", async () => { | 498 | it("responds to server.getConfig", async () => { |
| 499 | const fakeHome = makeTempDir("t3code-home-"); | 499 | const stateDir = makeTempDir("t3code-state-get-config-"); |
| 500 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | 500 | fs.writeFileSync(path.join(stateDir, "keybindings.json"), "[]", "utf8"); |
| 501 | server = await createTestServer({ cwd: "/my/workspace" }); | 501 | |
| | 502 | server = await createTestServer({ cwd: "/my/workspace", stateDir }); |
| 502 | const addr = server.address(); | 503 | const addr = server.address(); |
| 503 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | 504 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
| 504 | | 505 | |
| ... | @@ -542,12 +543,10 @@ describe("WebSocket Server", () => { | ... | @@ -542,12 +543,10 @@ describe("WebSocket Server", () => { |
| 542 | expect(openCalls).toEqual([{ cwd: "/my/workspace", editor: "cursor" }]); | 543 | expect(openCalls).toEqual([{ cwd: "/my/workspace", editor: "cursor" }]); |
| 543 | }); | 544 | }); |
| 544 | | 545 | |
| 545 | it("reads keybindings from ~/.t3/keybindings.json", async () => { | 546 | it("reads keybindings from the configured state directory", async () => { |
| 546 | const fakeHome = makeTempDir("t3code-home-"); | 547 | const stateDir = makeTempDir("t3code-state-keybindings-"); |
| 547 | const configDir = path.join(fakeHome, ".t3"); | | |
| 548 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 549 | fs.writeFileSync( | 548 | fs.writeFileSync( |
| 550 | path.join(configDir, "keybindings.json"), | 549 | path.join(stateDir, "keybindings.json"), |
| 551 | JSON.stringify([ | 550 | JSON.stringify([ |
| 552 | { key: "cmd+j", command: "terminal.toggle" }, | 551 | { key: "cmd+j", command: "terminal.toggle" }, |
| 553 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, | 552 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| ... | @@ -555,9 +554,7 @@ describe("WebSocket Server", () => { | ... | @@ -555,9 +554,7 @@ describe("WebSocket Server", () => { |
| 555 | ]), | 554 | ]), |
| 556 | "utf8", | 555 | "utf8", |
| 557 | ); | 556 | ); |
| 558 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | 557 | server = await createTestServer({ cwd: "/my/workspace", stateDir }); |
| 559 | | | |
| 560 | server = await createTestServer({ cwd: "/my/workspace" }); | | |
| 561 | const addr = server.address(); | 558 | const addr = server.address(); |
| 562 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | 559 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
| 563 | | 560 | |
| ... | @@ -578,128 +575,16 @@ describe("WebSocket Server", () => { | ... | @@ -578,128 +575,16 @@ describe("WebSocket Server", () => { |
| 578 | }); | 575 | }); |
| 579 | }); | 576 | }); |
| 580 | | 577 | |
| 581 | it("warns and ignores invalid keybinding entries", async () => { | | |
| 582 | const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); | | |
| 583 | const fakeHome = makeTempDir("t3code-home-invalid-entry-"); | | |
| 584 | const configDir = path.join(fakeHome, ".t3"); | | |
| 585 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 586 | fs.writeFileSync( | | |
| 587 | path.join(configDir, "keybindings.json"), | | |
| 588 | JSON.stringify([ | | |
| 589 | { key: "mod+j", command: "terminal.toggle" }, | | |
| 590 | { key: "mod+z", command: "invalid.command", when: "terminalFocus" }, | | |
| 591 | ]), | | |
| 592 | "utf8", | | |
| 593 | ); | | |
| 594 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | | |
| 595 | | | |
| 596 | server = await createTestServer({ cwd: "/my/workspace" }); | | |
| 597 | const addr = server.address(); | | |
| 598 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | | |
| 599 | | | |
| 600 | const ws = await connectWs(port); | | |
| 601 | connections.push(ws); | | |
| 602 | | | |
| 603 | await waitForMessage(ws); | | |
| 604 | | | |
| 605 | const response = await sendRequest(ws, WS_METHODS.serverGetConfig); | | |
| 606 | expect(response.error).toBeUndefined(); | | |
| 607 | expect(response.result).toEqual({ | | |
| 608 | cwd: "/my/workspace", | | |
| 609 | keybindings: mergeWithDefaultsForTest([{ key: "mod+j", command: "terminal.toggle" }]), | | |
| 610 | }); | | |
| 611 | expect( | | |
| 612 | warnSpy.mock.calls.some(([message]) => | | |
| 613 | String(message).includes("ignoring invalid keybinding entries"), | | |
| 614 | ), | | |
| 615 | ).toBe(true); | | |
| 616 | }); | | |
| 617 | | | |
| 618 | it("warns and ignores keybindings with malformed when expressions", async () => { | | |
| 619 | const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); | | |
| 620 | const fakeHome = makeTempDir("t3code-home-invalid-when-"); | | |
| 621 | const configDir = path.join(fakeHome, ".t3"); | | |
| 622 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 623 | fs.writeFileSync( | | |
| 624 | path.join(configDir, "keybindings.json"), | | |
| 625 | JSON.stringify([{ key: "mod+j", command: "terminal.toggle", when: "terminalFocus && (" }]), | | |
| 626 | "utf8", | | |
| 627 | ); | | |
| 628 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | | |
| 629 | | | |
| 630 | server = await createTestServer({ cwd: "/my/workspace" }); | | |
| 631 | const addr = server.address(); | | |
| 632 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | | |
| 633 | | | |
| 634 | const ws = await connectWs(port); | | |
| 635 | connections.push(ws); | | |
| 636 | | | |
| 637 | await waitForMessage(ws); | | |
| 638 | | | |
| 639 | const response = await sendRequest(ws, WS_METHODS.serverGetConfig); | | |
| 640 | expect(response.error).toBeUndefined(); | | |
| 641 | expect(response.result).toEqual({ | | |
| 642 | cwd: "/my/workspace", | | |
| 643 | keybindings: DEFAULT_RESOLVED_KEYBINDINGS, | | |
| 644 | }); | | |
| 645 | expect( | | |
| 646 | warnSpy.mock.calls.some(([message]) => | | |
| 647 | String(message).includes("ignoring invalid keybinding entries"), | | |
| 648 | ), | | |
| 649 | ).toBe(true); | | |
| 650 | }); | | |
| 651 | | | |
| 652 | it("reads keybindings once at startup and caches the resolved config", async () => { | | |
| 653 | const fakeHome = makeTempDir("t3code-home-cached-config-"); | | |
| 654 | const configDir = path.join(fakeHome, ".t3"); | | |
| 655 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 656 | const keybindingsPath = path.join(configDir, "keybindings.json"); | | |
| 657 | fs.writeFileSync( | | |
| 658 | keybindingsPath, | | |
| 659 | JSON.stringify([{ key: "cmd+j", command: "terminal.toggle" }]), | | |
| 660 | "utf8", | | |
| 661 | ); | | |
| 662 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | | |
| 663 | | | |
| 664 | server = await createTestServer({ cwd: "/my/workspace" }); | | |
| 665 | const addr = server.address(); | | |
| 666 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | | |
| 667 | | | |
| 668 | const ws = await connectWs(port); | | |
| 669 | connections.push(ws); | | |
| 670 | await waitForMessage(ws); | | |
| 671 | | | |
| 672 | const firstResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); | | |
| 673 | expect(firstResponse.error).toBeUndefined(); | | |
| 674 | expect(firstResponse.result).toEqual({ | | |
| 675 | cwd: "/my/workspace", | | |
| 676 | keybindings: mergeWithDefaultsForTest([{ key: "cmd+j", command: "terminal.toggle" }]), | | |
| 677 | }); | | |
| 678 | | | |
| 679 | fs.writeFileSync( | | |
| 680 | keybindingsPath, | | |
| 681 | JSON.stringify([{ key: "cmd+k", command: "terminal.toggle" }]), | | |
| 682 | "utf8", | | |
| 683 | ); | | |
| 684 | | | |
| 685 | const secondResponse = await sendRequest(ws, WS_METHODS.serverGetConfig); | | |
| 686 | expect(secondResponse.error).toBeUndefined(); | | |
| 687 | expect(secondResponse.result).toEqual(firstResponse.result); | | |
| 688 | }); | | |
| 689 | | | |
| 690 | it("upserts keybinding rules and updates cached server config", async () => { | 578 | it("upserts keybinding rules and updates cached server config", async () => { |
| 691 | const fakeHome = makeTempDir("t3code-home-upsert-keybinding-"); | 579 | const stateDir = makeTempDir("t3code-state-upsert-keybinding-"); |
| 692 | const configDir = path.join(fakeHome, ".t3"); | 580 | const keybindingsPath = path.join(stateDir, "keybindings.json"); |
| 693 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 694 | const keybindingsPath = path.join(configDir, "keybindings.json"); | | |
| 695 | fs.writeFileSync( | 581 | fs.writeFileSync( |
| 696 | keybindingsPath, | 582 | keybindingsPath, |
| 697 | JSON.stringify([{ key: "mod+j", command: "terminal.toggle" }]), | 583 | JSON.stringify([{ key: "mod+j", command: "terminal.toggle" }]), |
| 698 | "utf8", | 584 | "utf8", |
| 699 | ); | 585 | ); |
| 700 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | | |
| 701 | | 586 | |
| 702 | server = await createTestServer({ cwd: "/my/workspace" }); | 587 | server = await createTestServer({ cwd: "/my/workspace", stateDir }); |
| 703 | const addr = server.address(); | 588 | const addr = server.address(); |
| 704 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | 589 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
| 705 | | 590 | |
| ... | @@ -739,70 +624,6 @@ describe("WebSocket Server", () => { | ... | @@ -739,70 +624,6 @@ describe("WebSocket Server", () => { |
| 739 | }); | 624 | }); |
| 740 | }); | 625 | }); |
| 741 | | 626 | |
| 742 | it("warns and ignores unsupported keybindings config format", async () => { | | |
| 743 | const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); | | |
| 744 | const fakeHome = makeTempDir("t3code-home-unsupported-format-"); | | |
| 745 | const configDir = path.join(fakeHome, ".t3"); | | |
| 746 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 747 | fs.writeFileSync( | | |
| 748 | path.join(configDir, "keybindings.json"), | | |
| 749 | JSON.stringify({ "terminal.toggle": "mod+j" }), | | |
| 750 | "utf8", | | |
| 751 | ); | | |
| 752 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | | |
| 753 | | | |
| 754 | server = await createTestServer({ cwd: "/my/workspace" }); | | |
| 755 | const addr = server.address(); | | |
| 756 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | | |
| 757 | | | |
| 758 | const ws = await connectWs(port); | | |
| 759 | connections.push(ws); | | |
| 760 | | | |
| 761 | await waitForMessage(ws); | | |
| 762 | | | |
| 763 | const response = await sendRequest(ws, WS_METHODS.serverGetConfig); | | |
| 764 | expect(response.error).toBeUndefined(); | | |
| 765 | expect(response.result).toEqual({ | | |
| 766 | cwd: "/my/workspace", | | |
| 767 | keybindings: DEFAULT_RESOLVED_KEYBINDINGS, | | |
| 768 | }); | | |
| 769 | expect( | | |
| 770 | warnSpy.mock.calls.some(([message]) => | | |
| 771 | String(message).includes("unsupported format; expected array"), | | |
| 772 | ), | | |
| 773 | ).toBe(true); | | |
| 774 | }); | | |
| 775 | | | |
| 776 | it("warns and ignores malformed keybindings config files", async () => { | | |
| 777 | const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); | | |
| 778 | const fakeHome = makeTempDir("t3code-home-invalid-"); | | |
| 779 | const configDir = path.join(fakeHome, ".t3"); | | |
| 780 | fs.mkdirSync(configDir, { recursive: true }); | | |
| 781 | fs.writeFileSync(path.join(configDir, "keybindings.json"), "{not-json", "utf8"); | | |
| 782 | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); | | |
| 783 | | | |
| 784 | server = await createTestServer({ cwd: "/my/workspace" }); | | |
| 785 | const addr = server.address(); | | |
| 786 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; | | |
| 787 | | | |
| 788 | const ws = await connectWs(port); | | |
| 789 | connections.push(ws); | | |
| 790 | | | |
| 791 | await waitForMessage(ws); | | |
| 792 | | | |
| 793 | const response = await sendRequest(ws, WS_METHODS.serverGetConfig); | | |
| 794 | expect(response.error).toBeUndefined(); | | |
| 795 | expect(response.result).toEqual({ | | |
| 796 | cwd: "/my/workspace", | | |
| 797 | keybindings: DEFAULT_RESOLVED_KEYBINDINGS, | | |
| 798 | }); | | |
| 799 | expect( | | |
| 800 | warnSpy.mock.calls.some(([message]) => | | |
| 801 | String(message).includes("ignoring malformed keybindings config"), | | |
| 802 | ), | | |
| 803 | ).toBe(true); | | |
| 804 | }); | | |
| 805 | | | |
| 806 | it("returns error for unknown methods", async () => { | 627 | it("returns error for unknown methods", async () => { |
| 807 | server = await createTestServer({ cwd: "/test" }); | 628 | server = await createTestServer({ cwd: "/test" }); |
| 808 | const addr = server.address(); | 629 | const addr = server.address(); |