Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .claude/skills/fix-issue/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Skip the gate for a contained single-file fix whose cause is proven and whose fi
- **The CHANGELOG entry is a fragment, not a sentence.** You will have just finished a deep investigation, and the pull to write it all down lands here, on the one artifact that has no room for it. Name the bug you fixed or the thing you added, one line, aiming under 120 characters: `Empty grid on a background tab whose rows were freed.` Never `X now does Y instead of Z`, and never a trailing `so ...` clause. The cause, the mechanism and the before-and-after go in the PR body, which is where a reviewer looks for them anyway. Add one entry per change, under one existing `### ` heading, and never open a second heading of a type the version already has. Entries written to this rule survive the release untouched; 0.67.0's did not, and all 211 were rewritten at release time.
- **Read `docs/STYLE.md` before you write a docs page, and write the page last.** It is the spec, it is 341 lines, and nothing else names it. Two failure modes it exists to stop both survive a green local run: a page that restates its own frontmatter description, repeats a caption as its alt text, argues design rationale at the reader, or makes the product the subject; and a capability table that was true when it was typed and false by the time the branch was pushed, because a later commit changed the code and nobody re-read the page. `.claude/rules/docs-authoring.md` carries the full list.
- **A docs page that describes UI carries a screenshot.** Prose alone does not show a user what a pane looks like, and every existing feature page in `docs/features/` pairs its description with one. When the change adds a screen, pane, tab, dialog or toolbar, add a `<Frame>` with the light and dark pair the docs use, `docs/images/<name>.png` and `docs/images/<name>-dark.png`, referenced as `className="block dark:hidden"` and `className="hidden dark:block"`. When the change alters a screen an existing page already pictures, the old shot is now wrong: re-capture it or say in the PR that it needs re-capturing. Real shots come from a running Debug build driven with `osascript` and captured with `screencapture`, launched with `TABLEPRO_UI_TEST_SANDBOX` pointed at a throwaway directory so it never touches your own connections; Screen Recording has to be granted to whatever runs it. When you cannot capture one, still add the `<Frame>` and commit a placeholder at the same 1560x960 the other shots use so the page renders, and call it out in the PR body as pending. Never leave the markup pointing at a file that does not exist; a broken image ships to the docs site.
- **Write the tests the blueprint specified**, unit and UI both. UI suites subclass `UITestCase`; a bare `XCUIApplication()` or `: XCTestCase` under `TableProUITests/` fails a source-scanning guard test, because storage isolation depends on the launch path. When a test fails, fix the source. Never bend a test to match wrong output.
- **Write the tests the blueprint specified**, unit and UI both. UI suites subclass `UITestCase`; a bare `XCUIApplication()` or `: XCTestCase` under `TableProUITests/` fails a source-scanning guard test, because storage isolation depends on the launch path. A unit test type in `TableProTests` gets no `@Suite` unless it needs a trait such as `.serialized`: each top-level `@Suite` adds compile time to the test module quadratically, and Repo Hygiene rejects one without a trait. When a test fails, fix the source. Never bend a test to match wrong output.
- **Run `Skill(swiftui-pro)`** when the change adds or reworks SwiftUI views, before you consider the code done.

## Phase 4: Verify
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/repo-hygiene.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ on:
- "Plugins/**/*.swift"
- "TablePro/**/*.swift"
- "Packages/**/*.swift"
- "TableProTests/**/*.swift"
- "project.yml"
- "TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
- "TablePro/Resources/ThirdPartyLicenses/licenses.yml"
Expand All @@ -34,6 +35,7 @@ on:
- "Plugins/**/*.swift"
- "TablePro/**/*.swift"
- "Packages/**/*.swift"
- "TableProTests/**/*.swift"
- "project.yml"
- "TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
- "TablePro/Resources/ThirdPartyLicenses/licenses.yml"
Expand Down Expand Up @@ -118,6 +120,16 @@ jobs:
- name: Validate the log privacy check
run: python3 scripts/ci/test_check_log_privacy.py

# Every top-level @Suite is a peer macro the compiler files in module-scope lookup, and each
# @Suite expansion walks all of them, so the test module's compile time grows with the square
# of their count. 2,336 of them held TableProTests' emit-module job at 544 to 626 seconds on the
# build runner, and 2,200 of those only renamed their type. Only a @Suite with a trait is kept.
- name: Check no top-level test type carries a @Suite without a trait
run: python3 scripts/ci/check-test-suite-attributes.py

- name: Validate the test suite attribute check
run: python3 scripts/ci/test_check_test_suite_attributes.py

# A plugin's String(localized:) resolves against Bundle.main, which is the host app, so its
# strings live in the app's catalog. Xcode extracts per target and the plugin targets are not
# the app target, so nothing puts them there: 537 strings had never reached a translator.
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ These have caused real production bugs:
- **Never call `ensureLayout(forCharacterRange:)`**: defeats `allowsNonContiguousLayout`. Let layout manager queries trigger lazy local layout.
- **SQL dumps can have single lines with millions of characters**: cap regex/highlight ranges at 10k chars.
- **Tab persistence**: a query longer than `TabQueryContent.maxPersistableQuerySize` (500,000 UTF-16 units) is blanked by `QueryTab.toPersistedTab()` to prevent a JSON freeze, and the full text moves to `TabQueryOverflowStore`. `RecentlyClosedTabStore` applies the same cap.
- **Never put a top-level `@Suite` on a test type in `TableProTests` unless it carries a trait**: Swift Testing finds a type's `@Test` functions without one, so `@Suite("Some name")` only renames the type in the test navigator, and it costs the test module compile time quadratically in that module's own count of top-level suites. `@Suite` is a peer macro, the compiler files every top-level one under a single module-scope placeholder for macro-generated unique names, and each `@Suite` expansion resolves its own unique names through that placeholder, expanding every other top-level `@Suite` in the module to do it. Measured on the macOS Tests build job: 2,336 of them, 2,200 carrying only a display name, held the `TableProTests` emit-module job at 544 to 626 seconds, the longest compile job in the build; dropping 2,173 of those 2,200 took it to 290 seconds on the same runner and the target's compile batches from 3,958 to 2,373 seconds summed. Keep `@Suite` for a trait (`.serialized`, `.enabled(if:)`). Nesting a suite to keep its name is not free either: it avoids the quadratic term but still adds emit-module time that grows linearly, about 60% more at 2,000 suites in a standalone reproducer, so leave the type unannotated. A display name on `@Test` adds nothing, since the `@Test` expands either way. The other test modules hold 108 top-level suites or fewer, where the quadratic term is under a second, which is why `scripts/ci/check-test-suite-attributes.py` fails Repo Hygiene on a new one in `TableProTests` only (swiftlang/swift#92638).

## Writing Style

Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/AWSConfigFileTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS config and credentials file resolution")
struct AWSConfigFileTests {
private let config = """
[default]
Expand Down
8 changes: 0 additions & 8 deletions TableProTests/AWS/AWSIAMAuthTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import Testing

@testable import TablePro

@Suite("AWS SigV4 primitives")
struct AWSSigV4Tests {
@Test("SHA-256 matches NIST vectors")
func sha256Vectors() {
Expand All @@ -38,7 +37,6 @@ struct AWSSigV4Tests {
}
}

@Suite("RDS auth token")
struct RDSAuthTokenGeneratorTests {
private let credentials = AWSCredentials(
accessKeyId: "AKIDEXAMPLE",
Expand Down Expand Up @@ -90,7 +88,6 @@ struct RDSAuthTokenGeneratorTests {
}
}

@Suite("RDS endpoint region")
struct RDSEndpointTests {
@Test("Derives region from cluster hostname")
func clusterHostname() {
Expand All @@ -114,7 +111,6 @@ struct RDSEndpointTests {
}
}

@Suite("RDS signing endpoint")
struct RDSSigningEndpointResolverTests {
private func resolve(
host: String = "mydb.abc123.us-east-1.rds.amazonaws.com",
Expand Down Expand Up @@ -255,7 +251,6 @@ struct RDSSigningEndpointResolverTests {
}
}

@Suite("AWS credential resolver")
struct AWSCredentialResolverTests {
@Test("Resolves static access-key credentials")
func staticCredentials() async throws {
Expand Down Expand Up @@ -285,7 +280,6 @@ struct AWSCredentialResolverTests {
}
}

@Suite("AWS config INI parsing")
struct AWSSSOParsingTests {
private let config = """
[default]
Expand Down Expand Up @@ -335,7 +329,6 @@ struct AWSSSOParsingTests {
}
}

@Suite("AWS IAM connection fields in the plugin metadata registry")
@MainActor
struct RegistryAWSIAMFieldsTests {
private func fieldIds(forTypeId typeId: String) -> [String] {
Expand Down Expand Up @@ -387,7 +380,6 @@ struct RegistryAWSIAMFieldsTests {
}
}

@Suite("AWS credential_process")
struct AWSCredentialProcessTests {
@Test("Tokenizes a plain command into arguments")
func tokenizePlain() {
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/AWSProfileResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS profile credential source")
struct AWSProfileResolutionTests {
@Test("A profile resolves by what it declares, in the AWS SDK's order")
func credentialSourceOrder() {
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/AWSQueryRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS query request signing")
struct AWSQueryRequestTests {
private static let credentials = AWSCredentials(
accessKeyId: "AKIAIOSFODNN7EXAMPLE",
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/AWSSSOLoginTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS SSO device login")
struct AWSSSOLoginTests {
@Test("Parses a device authorization response, defaulting the poll interval")
func parsesDeviceAuthorization() throws {
Expand Down
4 changes: 0 additions & 4 deletions TableProTests/AWS/AWSSSOResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ sso_account_id = 222222222222
sso_role_name = LegacyRole
"""

@Suite("AWSSSO - INI parsing")
struct AWSSSOIniParsingTests {
@Test("comment lines and empty lines are skipped")
func skipsCommentsAndEmptyLines() {
Expand Down Expand Up @@ -57,7 +56,6 @@ struct AWSSSOIniParsingTests {
}
}

@Suite("AWSSSO - parseProfileSettings")
struct AWSSSOProfileSettingsTests {
@Test("modern profile resolves all fields from sso-session block")
func resolvesModernProfile() throws {
Expand Down Expand Up @@ -118,7 +116,6 @@ struct AWSSSOProfileSettingsTests {
}
}

@Suite("AWSSSO - readAccessToken")
struct AWSSSOTokenCacheTests {
private func makeCacheDirectory() throws -> String {
let dir = NSTemporaryDirectory() + "AWSSSOTokenCacheTests_\(UUID().uuidString)/"
Expand Down Expand Up @@ -258,7 +255,6 @@ private final class AWSSSOStubProtocol: URLProtocol, @unchecked Sendable {
}
}

@Suite("AWSSSO - fetchRoleCredentials")
struct AWSSSOFetchTests {
private let settings = AWSSSOProfileSettings(
accountId: "111111111111",
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/AWSSSOTokenCacheWritingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS SSO token cache writing")
struct AWSSSOTokenCacheWritingTests {
private func makeCacheDirectory() throws -> String {
let directory = FileManager.default.temporaryDirectory
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/AWSSTSTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS STS AssumeRole response parsing")
struct AWSSTSTests {
private let validResponse = """
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/ElastiCacheAuthTokenTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("ElastiCache IAM auth token")
struct ElastiCacheAuthTokenTests {
private let credentials = AWSCredentials(
accessKeyId: "AKIDEXAMPLE",
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/KeyspacesSigV4Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AWS Keyspaces SigV4 authentication")
struct KeyspacesSigV4Tests {
private let credentials = AWSCredentials(
accessKeyId: "AKIDEXAMPLE",
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/RDSConnectionBuilderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import TableProImport
import TableProPluginKit
import Testing

@Suite("RDS connection building")
struct RDSConnectionBuilderTests {
private static let iamAuthentication = AWSDiscoveryAuthentication(
mode: .iam,
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/RDSDescribeResponseParserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
@testable import TablePro
import Testing

@Suite("RDS describe response parsing")
struct RDSDescribeResponseParserTests {
static let instancesXML = """
<DescribeDBInstancesResponse xmlns="http://rds.amazonaws.com/doc/2014-10-31/">
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/RDSDiscoveryPlanTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Foundation
@testable import TablePro
import Testing

@Suite("RDS discovery plan")
struct RDSDiscoveryPlanTests {
private func instance(
identifier: String,
Expand Down
1 change: 0 additions & 1 deletion TableProTests/AWS/RDSDiscoveryReconcilerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import Foundation
import TableProImport
import Testing

@Suite("RDS discovery reconciliation")
struct RDSDiscoveryReconcilerTests {
private func exportable(
name: String = "orders",
Expand Down
2 changes: 0 additions & 2 deletions TableProTests/Accessibility/AccessibleControlNameTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import Testing
/// reads hints only after a delay, if the user has not turned them off. So a button carrying a
/// perfectly good localized string in `.help()` and nothing in `.accessibilityLabel()` announces
/// its SF Symbol name, or nothing at all.
@Suite("Accessible control names")
struct AccessibleControlNameTests {
/// Every one of these had a name in `.help()` and none in `.accessibilityLabel()`. The pairs
/// are (file, the label string that must appear in it), so the test fails if a label is dropped
Expand Down Expand Up @@ -106,7 +105,6 @@ struct AccessibleControlNameTests {
/// measured, so a sorted column is announced only when its header cell is told directly. The plan
/// outline gives every column a `sortDescriptorPrototype`, so all of them are click-sortable, and
/// none of them said which way it was sorted.
@Suite("Query plan sort direction")
@MainActor
struct QueryPlanSortDirectionTests {
@Test("The sorted column publishes its direction and the others publish none")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Testing

@testable import TablePro

@Suite("Cloud SQL Auth Proxy binary manager")
struct CloudSQLProxyBinaryManagerTests {
private func makeTempDirectory() throws -> URL {
let url = FileManager.default.temporaryDirectory
Expand Down
1 change: 0 additions & 1 deletion TableProTests/CloudSQL/CloudSQLProxyModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Testing

@testable import TablePro

@Suite("Cloud SQL Auth Proxy model")
struct CloudSQLProxyModelTests {
@Test("CloudSQLProxyConfiguration round-trips through Codable")
func configurationRoundTrip() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Testing
@testable import TablePro

@MainActor
@Suite("Cloud SQL Auth Proxy pane view model")
struct CloudSQLProxyPaneViewModelTests {
@Test("disabled reports no issues")
func disabledNoIssues() {
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Cloudflare/CloudflareModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Testing

@testable import TablePro

@Suite("Cloudflare tunnel model")
struct CloudflareModelTests {
@Test("CloudflareConfiguration round-trips through Codable")
func configurationRoundTrip() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Testing

@testable import TablePro

@Suite("Cloudflare tunnel pane validation")
@MainActor
struct CloudflareTunnelPaneViewModelTests {
@Test("disabled tunnel reports no validation issues")
Expand Down
2 changes: 0 additions & 2 deletions TableProTests/Core/AI/AIChatInlineSourceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ private final class RecordingTransport: ChatTransport, @unchecked Sendable {
func testConnection() async throws -> Bool { true }
}

@Suite("AI chat inline source honours the connection's AI policy")
@MainActor
internal struct AIChatInlineSourceTests {
private let connectionId = UUID()
Expand Down Expand Up @@ -161,7 +160,6 @@ internal struct AIChatInlineSourceTests {
}
}

@Suite("Inline suggestion source kind")
internal struct InlineSuggestionSourceKindTests {
private func settings(providerType: AIProviderType, inlineEnabled: Bool = true) -> AISettings {
let provider = AIProviderConfig(name: "Test", type: providerType)
Expand Down
3 changes: 0 additions & 3 deletions TableProTests/Core/AI/AIConnectionAccessGateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ private final class PolicyBox {
}
}

@Suite("AI connection access gate")
@MainActor
internal struct AIConnectionAccessGateTests {
private func makeGate(_ box: PolicyBox, approvals: AIAccessApprovals) -> AIConnectionAccessGate {
Expand Down Expand Up @@ -90,7 +89,6 @@ internal struct AIConnectionAccessGateTests {
}
}

@Suite("AI connection access gate reading the saved connection")
@MainActor
internal struct AIConnectionAccessGateSavedPolicyTests {
private let storage: ConnectionStorage
Expand Down Expand Up @@ -195,7 +193,6 @@ internal struct AIConnectionAccessGateSavedPolicyTests {
}
}

@Suite("AI access approvals across the chat and the session")
@MainActor
internal struct AIAccessApprovalsSessionTests {
@Test("Ending a connection's session revokes its approval and leaves the others")
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIEndpointTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Testing

@testable import TablePro

@Suite("AI Endpoint Resolution")
struct AIEndpointTests {
private func chatURL(_ configured: String, _ style: AIEndpointStyle, model: String = "m") -> String? {
AIEndpoint(configured, style: style)?.chatURL(model: model, style: style)?.absoluteString
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIModelCatalogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Foundation
@testable import TablePro
import Testing

@Suite("AI model catalog")
struct AIModelCatalogTests {
private let claude = AIProviderType.claude.rawValue

Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIModelListFetchGateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import Testing

@testable import TablePro

@Suite("AI model list fetch gate")
struct AIModelListFetchGateTests {
@Test("A provider that cannot fetch a model list is blocked whatever the key")
func blocksWhenNotFetchable() {
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIProviderCapabilitiesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Foundation
@testable import TablePro
import Testing

@Suite("AIProviderDescriptor capabilities")
struct AIProviderCapabilitiesTests {
init() {
AIProviderRegistration.registerAll()
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIProviderErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import Testing

@testable import TablePro

@Suite("AIProviderError.isRetryable")
struct AIProviderErrorTests {
@Test("Transient transport failures are retryable")
func transientErrorsAreRetryable() {
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIProviderFactoryCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import TableProPluginKit
@testable import TablePro
import Testing

@Suite("AIProviderFactory cache")
@MainActor
struct AIProviderFactoryCacheTests {
private func makeConfig(
Expand Down
1 change: 0 additions & 1 deletion TableProTests/Core/AI/AIProviderFactoryResolveTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import Foundation
import TableProPluginKit
import Testing

@Suite("AIProviderFactory.resolve")
@MainActor
struct AIProviderFactoryResolveTests {
/// Each test uses a unique provider id so the factory cache (keyed by id)
Expand Down
Loading
Loading