| ... | ... | @@ -496,9 +496,10 @@ describe("WebSocket Server", () => { |
| 496 | 496 | }); |
| 497 | 497 | |
| 498 | 498 | it("responds to server.getConfig", async () => { |
| 499 | | const fakeHome = makeTempDir("t3code-home-"); |
| 500 | | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); |
| 501 | | server = await createTestServer({ cwd: "/my/workspace" }); |
| 499 | const stateDir = makeTempDir("t3code-state-get-config-"); |
| 500 | fs.writeFileSync(path.join(stateDir, "keybindings.json"), "[]", "utf8"); |
| 501 | |
| 502 | server = await createTestServer({ cwd: "/my/workspace", stateDir }); |
| 502 | 503 | const addr = server.address(); |
| 503 | 504 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
| 504 | 505 | |
| ... | ... | @@ -542,12 +543,10 @@ describe("WebSocket Server", () => { |
| 542 | 543 | expect(openCalls).toEqual([{ cwd: "/my/workspace", editor: "cursor" }]); |
| 543 | 544 | }); |
| 544 | 545 | |
| 545 | | it("reads keybindings from ~/.t3/keybindings.json", async () => { |
| 546 | | const fakeHome = makeTempDir("t3code-home-"); |
| 547 | | const configDir = path.join(fakeHome, ".t3"); |
| 548 | | fs.mkdirSync(configDir, { recursive: true }); |
| 546 | it("reads keybindings from the configured state directory", async () => { |
| 547 | const stateDir = makeTempDir("t3code-state-keybindings-"); |
| 549 | 548 | fs.writeFileSync( |
| 550 | | path.join(configDir, "keybindings.json"), |
| 549 | path.join(stateDir, "keybindings.json"), |
| 551 | 550 | JSON.stringify([ |
| 552 | 551 | { key: "cmd+j", command: "terminal.toggle" }, |
| 553 | 552 | { key: "mod+d", command: "terminal.split", when: "terminalFocus" }, |
| ... | ... | @@ -555,9 +554,7 @@ describe("WebSocket Server", () => { |
| 555 | 554 | ]), |
| 556 | 555 | "utf8", |
| 557 | 556 | ); |
| 558 | | vi.spyOn(os, "homedir").mockReturnValue(fakeHome); |
| 559 | | |
| 560 | | server = await createTestServer({ cwd: "/my/workspace" }); |
| 557 | server = await createTestServer({ cwd: "/my/workspace", stateDir }); |
| 561 | 558 | const addr = server.address(); |
| 562 | 559 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
| 563 | 560 | |
| ... | ... | @@ -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 | 578 | it("upserts keybinding rules and updates cached server config", async () => { |
| 691 | | const fakeHome = makeTempDir("t3code-home-upsert-keybinding-"); |
| 692 | | const configDir = path.join(fakeHome, ".t3"); |
| 693 | | fs.mkdirSync(configDir, { recursive: true }); |
| 694 | | const keybindingsPath = path.join(configDir, "keybindings.json"); |
| 579 | const stateDir = makeTempDir("t3code-state-upsert-keybinding-"); |
| 580 | const keybindingsPath = path.join(stateDir, "keybindings.json"); |
| 695 | 581 | fs.writeFileSync( |
| 696 | 582 | keybindingsPath, |
| 697 | 583 | JSON.stringify([{ key: "mod+j", command: "terminal.toggle" }]), |
| 698 | 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 | 588 | const addr = server.address(); |
| 704 | 589 | const port = typeof addr === "object" && addr !== null ? addr.port : 0; |
| 705 | 590 | |
| ... | ... | @@ -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 | 627 | it("returns error for unknown methods", async () => { |
| 807 | 628 | server = await createTestServer({ cwd: "/test" }); |
| 808 | 629 | const addr = server.address(); |