Skip to content

refactor: enforce six-layer package boundaries and repay the recorded debt - #2055

Open
sang-neo03 wants to merge 36 commits into
mainfrom
refactor/package-debt-phase2
Open

refactor: enforce six-layer package boundaries and repay the recorded debt#2055
sang-neo03 wants to merge 36 commits into
mainfrom
refactor/package-debt-phase2

Conversation

@sang-neo03

@sang-neo03 sang-neo03 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Installs machine enforcement for the repository's six package dependency layers, then clears the reverse dependencies it records. The gate and the first round of cleanup land together: 39 existing violations are registered on arrival and 21 of them are repaid in the same change, leaving 18 shortcuts/*internal/* edges for a later step. Supersedes #2045, which carried the enforcement half on its own.

Changes

Enforcement

  • Add a data-driven layering test in internal/qualitygate/deptest. It runs go list -json ./... twice for each of the seven published GOOS/GOARCH combinations — untagged and with -tags authsidecar — and unions the resulting production dependency graphs. The union matters: extension/credential/sidecar and its denied edges only exist under the tag.
  • Enforce seven rules: extension must not reach internal/** (transitive); events must not reach shortcuts/** (transitive); packages under shortcuts/ must not import internal/auth, internal/keychain, internal/credential, internal/client or internal/vfs directly, reaching them through the shortcuts/common runtime gate instead — the 18 rows still registered are exactly these five; only the cmd assembly root and cmd/auth may import shortcuts/**; errs stays a repository leaf; internal/** must not import upper layers; and packages under extension/platform/examples/ may directly import only cmd and extension/platform.
  • Exempt by exact import path, and by exact (from, denied) edge where a package merely needs one or two of the denied imports: a whole-package exemption would also clear every denied import added to it later, so shortcuts/apps/gitcred and the manifest-export collector use edge exemptions while the runtime gate, the cmd assembly roots and the wrapper-main demos keep whole-package ones. The two wrapper-main demos are exempt from the transitive extension rule because their cmd import is the pattern they exist to demonstrate, while examples-surface-only keeps their direct surface pinned to two packages, so the inherited chain stays bounded.
  • Record pre-existing violations in layering-edges.txt and reconcile in both directions: an unregistered violation fails, and a registered edge that no longer exists fails with instructions to delete the row. CI additionally compares (from, denied) key sets with the base revision, so rows can only be removed — equal-count replacement fails too. The first-run bootstrap is pinned to an approved snapshot, hardcoded so the approved size cannot be raised from CI configuration without appearing in a diff.
  • Remove the circular-dependency report from arch-audit.yml; Go already rejects import cycles at compile time.

Cleanup

  • Remove extension's dependency on internal implementation (17 edges). Environment variable names move to a new repository-root leaf package envnames, which imports nothing: 6 of the 15 constants are read by both extension and internal layers, so a single home is the only way to keep one spelling per variable. internal/envvars keeps the 5 internal-only constants, and CliAgentName is deleted because nothing referenced it. core.ParseBrand is replaced by credential.ParseBrand, defined next to the Brand constants it maps to.
  • Move pure message conversion below shortcuts (4 edges). events reached up into shortcuts/im/convert_lib for three functions and one context type; the transitive closure of that entry point is now internal/imcontent, which depends on the standard library only. convert_lib keeps the code that needs a runtime context and delegates the pure part, projecting its richer ConvertContext down at the boundary rather than duplicating the conversion.
  • Extract authentication logging from internal/keychain into internal/authlog, constructed with explicit options instead of a replaceable package variable for the log directory. One instance is installed while the command factory is built — the only place that knows the workspace-aware runtime directory, since internal/core imports internal/keychain — so both callers share a single file handle and a single log prune per process.
  • Split internal/binding into internal/secaudit (path auditing) and internal/openclawbind (config binding and secret resolution). The halves depend one way, openclawbindsecaudit, and each existing caller uses exactly one of them.

Merging internal/appmeta into internal/event is deliberately out of scope: it changes no dependency and the design marks it optional.

Test Plan

  • go build ./... and go build -tags authsidecar ./...
  • go test ./internal/qualitygate/deptest/ -count=1 — the gate passes with 18 registered edges
  • bash scripts/check-layering-ratchet.sh main — passes against the approved 18-edge bootstrap snapshot
  • bash scripts/check-layering-ratchet.sh refactor/package-topology — deletions only, so the check also holds if the enforcement half merges first
  • make script-test, including the ratchet's own suite: unchanged rows, metadata-only edits, deletions and CRLF pass; new keys, equal-count replacement, malformed rows, padded fields, duplicate keys, a missing file and an invalid base revision all fail
  • go test ./cmd/... ./internal/... ./shortcuts/... ./extension/... -count=1 — 111 packages pass. internal/qualitygate/publiccontent fails in this working copy only, because a local .git/ai/ subtree blocks t.TempDir cleanup; the same commit passes in a clean worktree and the package is untouched here.
  • gofmt -l . and go vet ./... report nothing
  • Dead-code comparison against origin/main, matching the CI job, reports no newly unreachable functions
  • make examples-build still builds both plugin-SDK demos
  • A forbidden import added to events/im fails with from, denied and rule diagnostics; deleting an active row fails as a new violation; adding a row for a nonexistent edge fails with deletion guidance; adding a violation together with a matching row passes the Go test but fails the CI ratchet
  • events, errs and a cmd subpackage imported from examples/audit-observer each fail examples-surface-only; a new package under any examples/ directory under extension/ fails extension-zero-internal
  • internal/authlog tests pin that every caller observes one logger and that the installed logger wins over the fallback
  • Manual local verification — not run. Runtime behaviour is unchanged except for where authentication diagnostics are written; confirming that needs a workspace-scoped run with a failing keychain.

Related Issues

Summary by CodeRabbit

  • New Features

    • Added structured authentication logging for auth HTTP responses and local auth errors, with daily log files, auto-retention cleanup, and configurable log directory via LARKSUITE_CLI_LOG_DIR.
  • Bug Fixes

    • Improved chat message rendering by consolidating content conversion (including merge-forward handling, mentions, and timestamp formatting).
    • Standardized environment-variable naming across auth/credential flows and normalized brand parsing.
  • Quality Improvements

    • Enforced “layering ratchet” checks in CI and local validation scripts.
    • Removed the circular-dependency check step from the architecture audit workflow.

Add a data-driven architecture layering test that builds the full import
graph (go list -json -tags authsidecar) and evaluates six rules:

- extension must not depend on internal (transitive; keeps it extractable
  as a standalone SDK module)
- events must not depend on shortcuts (transitive)
- shortcuts must not directly import auth/keychain/credential/client/vfs
  (direct; must go through the shortcuts/common RuntimeContext gate)
- cmd subpackages must not import shortcuts (assembly point + cmd/auth only)
- errs must stay a leaf
- internal must not depend on cmd/shortcuts/events

Pre-existing violations are seeded into layering-edges.txt (37 rows). The
test rejects any unseeded violation (new debt) and any stale row (removed
debt), and CI locks the effective row count to only ever decrease. Removes
the tautological circular-dependency check from arch-audit.yml, since Go
already forbids import cycles at compile time.
Check all seven published GOOS and GOARCH combinations, and keep go list diagnostics separate from its JSON output for cold caches.\n\nMake the bootstrap snapshot immutable in CI, propagate shell failures explicitly, isolate sourced execution, and add deterministic regression tests for each contract.
Layering edge parsing and graph coverage:
- Reject whitespace-padded exception fields instead of silently trimming
  them, so a padded row is a malformed row rather than a coerced identity;
  add a padded-field parse test.
- Fail loud when any release target/tag combination lists zero packages,
  which would otherwise let the layering graph silently under-cover.
- Document the build-tag scope (demo tags excluded), the SkipFrom substring
  semantics, and the toolchain-derived support set behind the drift check.

GoReleaser drift checks:
- Reject custom build commands and per-target overrides as unsupported.
- Detect --tags in addition to -tags when rejecting release build tags.
- Reject any GO* build environment variable (except CGO_ENABLED=0) through a
  single default branch instead of an explicit allowlist.
- Validate the GoReleaser global env block, and make the go-list stderr test
  table-driven across the default and authsidecar graphs.
The extension rule skipped any package whose import path contained
"/examples/", which let the gate miss two things: a directory named
examples anywhere under extension escaped the rule outright, and the
sanctioned demos were exempt from every denial rather than only from the
internal packages they inherit through cmd.

- Drop SkipFrom (and containsAny) so no rule can exempt by directory name.
- Exempt the two wrapper-main demos from extension-zero-internal by exact
  import path. Their cmd import is the pattern they exist to demonstrate,
  and seeding those edges instead would wedge the ratchet: the edges track
  cmd's transitive set, so a new internal package under cmd would demand a
  new row that check-layering-ratchet.sh refuses by design.
- Add examples-surface-only: demos may consume cmd and extension/platform
  but must not directly import internal or shortcuts. Zero violations today.

layering-edges.txt stays at 39 rows, so the ratchet bootstrap snapshot
still matches.
examples-surface-only promised that demos may consume only the assembled CLI
and the public plugin SDK, but it enforced two denied prefixes instead, so
every tree nobody thought to deny was permitted. A demo importing `events`,
`errs` or a `cmd` subpackage passed the gate, and because
extension-zero-internal exempts these packages from the transitive check,
nothing examined what those imports dragged in either. The exemption was
therefore unbounded in what it covered, the same defect as the directory-name
skip it replaced.

- Add Rule.AllowedRepoDeps, which inverts the check: any dependency inside
  this module that is not listed is a violation. Standard library and
  third-party packages, including same-organisation modules that are not this
  one, stay outside the rule.
- Pin examples-surface-only to exactly `cmd` and `extension/platform`, so the
  rule name matches what it enforces and the inherited chain stays bounded by
  a direct surface of two packages.
- Cover the reproducers as contract cases: other repository trees, `cmd`
  subpackages, other `extension` subtrees, and the module root are rejected,
  while the two allowed packages plus non-module imports are not.

layering-edges.txt stays at 39 rows; the demos already import only the two
allowed packages.
Removing the internal/core dependency left each credential provider with its
own copy of the brand rule. Two identical five-line functions mean the brand
set can grow in one provider and silently not in the other, with nothing to
catch it at build time.

Move the rule next to the Brand constants as credential.ParseBrand and have
both providers call it. Same behaviour, one definition.
Once events switched to internal/imcontent directly, the convert_lib wrapper
for ConvertInteractiveEventContent had no callers left. It is newly unreachable
code, which the CI dead-code gate rejects because it only tolerates entries
that already exist on the base branch.
Extracting the logger from keychain replaced an injected package variable with
per-call construction, which regressed two things.

Keychain errors went to the wrong directory. cmdutil used to inject
core.GetRuntimeDir into keychain, so every auth diagnostic landed in the
workspace-aware log. keychain now built its logger with empty Options, falling
back to the pre-workspace ~/.lark-cli path while internal/auth kept passing
core.GetRuntimeDir. Inside a workspace the two halves of one investigation
split across two directories, and LARKSUITE_CLI_LOG_DIR masks it whenever that
override is set.

Each call also built a fresh logger. The sync.Once guarding file creation is
per instance, so every logged line reopened the file — never closed — and
re-ran the week-old-log prune. wrapError fires on every keychain operation, and
a locked keychain is exactly the failure this log exists to diagnose.

Install one logger while the command factory is built, which is the only place
that knows the workspace-aware directory: authlog cannot resolve it itself
because internal/core imports internal/keychain, which imports authlog. Both
callers now read that shared instance, so there is one file handle and one
prune per process. Tests pin the singleton and the install-wins behaviour.

The process-wide variable is a stopgap: the internal/core split can hand the
runtime directory to authlog directly and remove the indirection.
@sang-neo03
sang-neo03 requested a review from liangshuo-1 as a code owner July 25, 2026 09:59
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds dependency-layering enforcement, public environment-name constants, centralized message conversion and authentication logging, and internal package namespace migrations. Callers, tests, workflows, and scripts are updated accordingly.

Changes

Layering quality gate

Layer / File(s) Summary
Dependency layering enforcement
.github/workflows/*, Makefile, internal/qualitygate/deptest/*, scripts/check-layering-ratchet*
Adds dependency-layering tests, seeded edges, ratchet validation, CI enforcement, and script coverage; removes circular dependency reporting from the architecture audit.

Shared environment and content contracts

Layer / File(s) Summary
Environment names and credential consumers
envnames/*, extension/credential/*, extension/transport/*, cmd/*, sidecar/*, main_noauthsidecar*, internal/envvars/*
Exports CLI environment-name constants, adds credential brand parsing, and migrates production code and tests from internal environment constants.
Message content conversion
internal/imcontent/*, shortcuts/im/convert_lib/*, events/im/message_receive.go
Centralizes content conversion, mention handling, timestamps, post extraction, and merge-forward parsing, then delegates shortcut and event conversion through imcontent.

Authentication logging and package boundaries

Layer / File(s) Summary
Shared authentication logger
internal/authlog/*, internal/auth/*, internal/cmdutil/*, internal/keychain/*
Adds configurable shared file logging and routes authentication responses and errors through injected logger instances.
Internal namespace migration
cmd/config/binder.go, internal/openclawbind/*, internal/secaudit/*, internal/transport/*
Renames internal binding and security-audit packages and updates OpenClaw, transport, and secure-path callers.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • larksuite/cli#235: Modifies the same authentication response logging call sites and logging plumbing.
  • larksuite/cli#515: Introduces the OpenClaw binding and secret-resolution implementation used by the binder migration.
  • larksuite/cli#2045: Directly overlaps with the layering ratchet, CI checks, seed data, and workflow changes.

Suggested labels: size/XL

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PR’s main focus on enforcing package boundaries and repaying recorded dependency debt.
Description check ✅ Passed The description follows the required template and includes a solid summary, change list, test plan, and related issue reference.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/package-debt-phase2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added domain/im PR touches the im domain size/XL Architecture-level or global-impact change labels Jul 25, 2026
@sang-neo03
sang-neo03 changed the base branch from refactor/package-topology to main July 25, 2026 10:03
…stry

The approved bootstrap snapshot records the registry size at the moment the
file first reaches the target branch, and it is the only check that runs in
that situation. This change now carries both the registry and the first round
of cleanup, so the file lands on main holding 18 edges rather than the 39 it
was pinned to.

Update the count and hash to match, and state in a comment why the baseline
stays hardcoded: a value CI could supply would let anyone raise the approved
size without the change appearing in a diff.
@sang-neo03 sang-neo03 changed the title refactor: clear the extension and events dependency inversions refactor: enforce six-layer package boundaries and repay the recorded debt Jul 25, 2026
main gained risk-control host signals, which changed the cachedHttpClientFunc
and cachedLarkClientFunc signatures and rewrote the proxy-warning test around
TestFactory. The only conflict was that test's import block: this branch moved
the shared environment variable names out of internal/envvars into envnames,
while main still imported the old package.

Keep envnames for the five constants that moved and add internal/core for the
config types the rewritten test now builds. internal/envvars is no longer
needed here; its remaining constants are the internal-only ones.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/authlog/authlog.go`:
- Around line 95-122: Update defaultRuntimeDir and Logger.logDir to validate
LARKSUITE_CLI_CONFIG_DIR and LARKSUITE_CLI_LOG_DIR with validate.SafeEnvDirPath,
and return a typed validation error when either explicit override is invalid
instead of falling back or redirecting. Adjust internal/authlog/authlog_test.go
lines 27-40 to assert the validation failure rather than the fallback directory
behavior.
- Around line 151-155: Update the argument formatting logic in authlog to avoid
persisting raw os.Args: retain command names while redacting sensitive flags and
their values, including app-secret, before joining and truncating the arguments
for the log. Ensure secrets are never emitted in the returned command string.

In `@internal/imcontent/helpers.go`:
- Around line 62-65: Update FormatTimestamp in internal/imcontent/helpers.go to
format timestamps at minute precision using the existing calendar date format,
preserving the HH:mm contract. Align the wrapper documentation or output
contract in shortcuts/im/convert_lib/helpers.go with this behavior, and add a
direct regression test covering the minute-precision result.

In `@sidecar/server-demo/handler_test.go`:
- Around line 423-428: The self-proxy validation in the run handlers must return
the prescribed typed validation/precondition error while preserving the
underlying cause, replacing the current fmt.Errorf path in both server-demo and
server-multi-tenant-demo main.go. Update TestRun_RejectsSelfProxy in
sidecar/server-demo/handler_test.go lines 423-428 and
sidecar/server-multi-tenant-demo/handler_test.go lines 419-424 to use
errs.ProblemOf and errors.As, asserting the typed error metadata and preserved
cause rather than only matching message text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a5ee3fe3-c391-46b9-b4bd-4ef390b2df39

📥 Commits

Reviewing files that changed from the base of the PR and between a7865cd and 602f671.

📒 Files selected for processing (84)
  • .github/workflows/arch-audit.yml
  • .github/workflows/ci.yml
  • Makefile
  • cmd/config/binder.go
  • cmd/root_integration_test.go
  • cmd/startup_brand.go
  • envnames/envnames.go
  • events/im/message_receive.go
  • extension/credential/env/env.go
  • extension/credential/env/env_test.go
  • extension/credential/sidecar/provider.go
  • extension/credential/sidecar/provider_test.go
  • extension/credential/types.go
  • extension/transport/sidecar/interceptor.go
  • internal/auth/app_registration.go
  • internal/auth/auth_response_log.go
  • internal/auth/device_flow.go
  • internal/auth/device_flow_test.go
  • internal/auth/revoke.go
  • internal/auth/uat_client.go
  • internal/auth/verify.go
  • internal/auth/verify_test.go
  • internal/authlog/authlog.go
  • internal/authlog/authlog_test.go
  • internal/cmdutil/factory_default.go
  • internal/cmdutil/factory_default_test.go
  • internal/cmdutil/factory_proxy_warn_test.go
  • internal/cmdutil/factory_test.go
  • internal/credential/integration_test.go
  • internal/envvars/envvars.go
  • internal/envvars/read.go
  • internal/envvars/read_test.go
  • internal/imcontent/card.go
  • internal/imcontent/card_test.go
  • internal/imcontent/card_userdsl.go
  • internal/imcontent/card_userdsl_test.go
  • internal/imcontent/convert.go
  • internal/imcontent/helpers.go
  • internal/imcontent/media.go
  • internal/imcontent/misc.go
  • internal/imcontent/text.go
  • internal/imcontent/text_test.go
  • internal/keychain/auth_log.go
  • internal/keychain/auth_log_test.go
  • internal/keychain/keychain.go
  • internal/openclawbind/json_pointer.go
  • internal/openclawbind/json_pointer_test.go
  • internal/openclawbind/lark_channel.go
  • internal/openclawbind/lark_channel_test.go
  • internal/openclawbind/reader.go
  • internal/openclawbind/reader_test.go
  • internal/openclawbind/secret_resolve.go
  • internal/openclawbind/secret_resolve_exec.go
  • internal/openclawbind/secret_resolve_exec_test.go
  • internal/openclawbind/secret_resolve_file.go
  • internal/openclawbind/secret_resolve_file_test.go
  • internal/openclawbind/secret_resolve_test.go
  • internal/openclawbind/tilde.go
  • internal/openclawbind/tilde_test.go
  • internal/openclawbind/types.go
  • internal/openclawbind/types_test.go
  • internal/qualitygate/deptest/layering-edges.txt
  • internal/qualitygate/deptest/layering_test.go
  • internal/secaudit/audit.go
  • internal/secaudit/audit_test.go
  • internal/secaudit/audit_unix.go
  • internal/secaudit/audit_windows.go
  • internal/secaudit/audit_windows_test.go
  • internal/transport/config.go
  • internal/transport/tls_ca.go
  • main_noauthsidecar.go
  • main_noauthsidecar_test.go
  • scripts/check-layering-ratchet.sh
  • scripts/check-layering-ratchet.test.sh
  • scripts/ci-workflow.test.sh
  • shortcuts/im/convert_lib/content_convert.go
  • shortcuts/im/convert_lib/content_media_misc_test.go
  • shortcuts/im/convert_lib/helpers.go
  • shortcuts/im/convert_lib/merge.go
  • shortcuts/im/convert_lib/pure_adapters.go
  • sidecar/server-demo/handler_test.go
  • sidecar/server-demo/main.go
  • sidecar/server-multi-tenant-demo/handler_test.go
  • sidecar/server-multi-tenant-demo/main.go
💤 Files with no reviewable changes (4)
  • internal/keychain/auth_log.go
  • internal/envvars/envvars.go
  • internal/keychain/auth_log_test.go
  • .github/workflows/arch-audit.yml

Comment thread internal/authlog/authlog.go
Comment thread internal/authlog/authlog.go Outdated
Comment on lines +62 to +65
if len(strings.TrimLeft(timestamp, "+-")) >= 13 {
value /= 1000
}
return time.Unix(value, 0).Local().Format("2006-01-02 15:04:05")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve the minute-precision timestamp contract.

FormatTimestamp now returns HH:mm:ss, but the migrated shortcut contract is HH:mm; calendar and todo output therefore changes during a refactor intended to centralize conversion. Restore 2006-01-02 15:04 and add a direct regression test.

  • internal/imcontent/helpers.go#L62-L65: format the centralized timestamp at minute precision.
  • shortcuts/im/convert_lib/helpers.go#L30-L31: keep the wrapper’s documented output contract aligned with the centralized implementation.

As per coding guidelines, “When transcribing input or transforming requests, preserve values faithfully.”

📍 Affects 2 files
  • internal/imcontent/helpers.go#L62-L65 (this comment)
  • shortcuts/im/convert_lib/helpers.go#L30-L31
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/imcontent/helpers.go` around lines 62 - 65, Update FormatTimestamp
in internal/imcontent/helpers.go to format timestamps at minute precision using
the existing calendar date format, preserving the HH:mm contract. Align the
wrapper documentation or output contract in shortcuts/im/convert_lib/helpers.go
with this behavior, and add a direct regression test covering the
minute-precision result.

Source: Coding guidelines

Comment thread sidecar/server-demo/handler_test.go Outdated
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@9c50045f14f2d91138f006768fe5cb739aebcb95

🧩 Skill update

npx skills add larksuite/cli#refactor/package-debt-phase2 -y -g

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.91026% with 97 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.99%. Comparing base (56c9a2a) to head (9c50045).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
internal/imcontent/helpers.go 31.25% 32 Missing and 1 partial ⚠️
internal/imcontent/convert.go 23.33% 23 Missing ⚠️
internal/authlog/authlog.go 84.61% 13 Missing and 9 partials ⚠️
extension/credential/types.go 0.00% 4 Missing ⚠️
internal/imcontent/misc.go 0.00% 4 Missing ⚠️
internal/auth/auth_response_log.go 80.00% 1 Missing and 1 partial ⚠️
internal/auth/device_flow.go 60.00% 2 Missing ⚠️
internal/auth/uat_client.go 0.00% 2 Missing ⚠️
internal/imcontent/text.go 0.00% 2 Missing ⚠️
shortcuts/im/convert_lib/pure_adapters.go 77.77% 1 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2055      +/-   ##
==========================================
- Coverage   75.15%   74.99%   -0.17%     
==========================================
  Files         912      915       +3     
  Lines       96453    96528      +75     
==========================================
- Hits        72492    72391     -101     
- Misses      18383    18583     +200     
+ Partials     5578     5554      -24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Two things surfaced once CI compared this branch against main.

The panic notice in cleanupOldLogs writes to os.Stderr, which forbidigo
rejects. The line is unchanged from internal/keychain, but moving the file
makes every line new to a diff-scoped linter. This package is a leaf with no
IOStreams in scope — the same constraint defaultRuntimeDir already documents —
and a panic in background cleanup still has to be visible, so mark it the way
internal/output marks its equivalent stderr write.

Coverage of the package sat at 28.6%: the extraction brought no test for what
the logger actually writes. Add three. One pins that lines land under the
supplied RuntimeDir, which is the property that regressed when a caller
constructed the logger with empty options. One covers the nil-receiver guard
both entry points carry. One pins the seven-day retention window and the
filename patterns the prune may touch. Coverage is now 80.5%.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/authlog/authlog_test.go`:
- Line 8: Replace direct filesystem calls in the authlog tests with the
corresponding internal/vfs APIs, including reads, writes, and stat operations in
the affected test sections. Remove the os filesystem dependency and ensure all
test filesystem access uses vfs so substituted vfs.DefaultFS is respected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 43347a06-d315-4541-8e87-1afb2738299f

📥 Commits

Reviewing files that changed from the base of the PR and between e5b2e96 and aec9c46.

📒 Files selected for processing (2)
  • internal/authlog/authlog.go
  • internal/authlog/authlog_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/authlog/authlog.go

Comment thread internal/authlog/authlog_test.go
Two packages were listed in ExceptFrom, which makes the evaluator skip the
source package before it looks at any dependency. gitcred needs keychain and
vfs; manifest-export needs the cmd root. Exempting them wholesale also cleared
every other denied import, so a later gitcred -> internal/client or
manifest-export -> events would pass the gate and never reach the registry. A
probe confirmed both slip through unreported.

Add ExceptEdges, matched on the exact (from, denied) pair, and move these two
across. ExceptFrom stays for packages whose whole job is to sit on the boundary
the rule draws: the shortcuts/common runtime gate, the cmd assembly roots, the
wrapper-main demos. Contract tests feed each package its allowed imports plus
one denied import and assert exactly one violation, so the allowed edges carry
weight instead of being asserted trivially.
The package documents one logger and one file handle per process, but SetShared
overwrote the current instance on every call and the factory can be built more
than once. A second construction opened a second file, left the first one open
with no way to close it, and moved later lines to whichever workspace directory
that construction resolved.

Only the first explicit install now takes effect. A lazily created fallback is
not an explicit install, so the first real one still replaces it — and closes
the file it had opened, which needs the handle to be retained rather than
handed to log.New and forgotten. The once-guarded init becomes a mutex so the
handle can be released safely; a closed logger drops writes instead of pointing
at a file nobody reads.

Tests cover two non-nil installs and the fallback handover, and both fail if
the guard is removed.
Removing extension's dependency on internal left the brand rule implemented
twice, once per Brand type. The two cannot share code: extension is published
as a standalone SDK and may not import internal, which is the constraint this
branch exists to establish. Cross-reference them so a third brand is added to
both rather than to whichever one the next author happens to open.

Also correct the timestamp wrapper's comment, which has claimed "HH:mm" since
before this branch while every implementation formatted seconds too.
The build-tag list was checked against a literal, so the test only caught edits
to the list itself. A tag introduced anywhere else left its files out of every
graph and the rules went quiet on them, with nothing to notice. The platform
list does not have this hole: it is cross-checked against .goreleaser.yml.

Walk the tree for //go:build constraints and require every custom tag to be
either unioned or listed as excluded. GOOS, GOARCH and toolchain terms come
from `go tool dist list` so a new port cannot look like a custom tag.

Exclusions now carry a reason that is verified rather than asserted: the two
demo tags are skipped because their files sit outside every rule's FromPrefix,
and the test fails if a file carrying one ever lands inside one. A listed
exclusion nobody uses fails too, so the list cannot rot.
…cted log dir

Two behaviours carried over from internal/keychain, both left as they were when
the package moved.

FormatAuthCmdline kept the first three arguments. That protected secrets only
while no sensitive flag appeared early: a global flag in front of the
subcommand put its value straight into a file that is world-readable to the
user, kept for seven days. Today's CLI cannot reach that state — the only
persistent flag is --profile and secrets arrive through --app-secret-stdin — so
this is about the shape, not a live leak. Drop everything from the first flag
onward instead. A denylist of sensitive names would need extending whenever one
is added; the command path is what the log is for, and it lives entirely in the
leading non-flag arguments. args[0] is reduced to its base name so an absolute
install path stays out too.

logDir swallowed the error when LARKSUITE_CLI_LOG_DIR failed validation and
wrote elsewhere while the caller kept watching the path they configured. Warn
instead. This fires only on a rejected override, not on every run, and logDir
resolves once per process.

Tests cover a flag ahead of the subcommand, the absolute-path case, and that a
usable override still prints nothing.
bf56e90 asked the next author to change both ParseBrand implementations by
cross-referencing them. That is the weakest kind of guarantee: it holds until
someone adds a brand to the package they happened to open.

Assert it instead. The constants are read out of both sources rather than
listed again, so a brand added to one package is under test immediately, and
the two parsers are compared on every declared value plus the inputs that
exercise the normalisation — case, padding, an unknown brand, a near miss.

The file sits in neither parser's package so neither owns the contract. It does
not merge the two implementations: extension/credential ships as a standalone
SDK and may not import internal, which is the constraint this branch exists to
establish.
Both demo entry points reported every startup failure with fmt.Errorf, so the
self-proxy rejection was indistinguishable from a missing key file except by
reading the sentence. Classify them instead: a poisoned environment and an
unreadable config are ConfigError, flag validation is ValidationError, local
key and log file work is InternalError, and listen or serve failures are
NetworkError. Causes are attached rather than folded into the message.

The self-proxy test asserts the type with errors.As and keeps the check that
the message names the variable, so renaming the variable still fails the test
while rewording the sentence no longer does.
…ntly

LARKSUITE_CLI_LOG_DIR is validated, LARKSUITE_CLI_CONFIG_DIR is not, and the
asymmetry looks like an oversight. It is not free to remove:
validate.SafeEnvDirPath resolves symlinks, so routing CONFIG_DIR through it
changes the directory the CLI reports on any host where the path crosses one.
Applying it moved four packages' expectations from /var to /private/var on
macOS. Whether config paths should be symlink-resolved is a decision about the
on-disk contract, not a local tidy-up, so say so where the next reader looks.

Also record why the stderr capture helper uses os while the file assertions use
vfs: os.Pipe and os.Stderr are process contracts with nothing for a substituted
filesystem to intercept.
475f04a stopped the command line at the first flag and dropped the
"keep three words" rule with it, on the reasoning that the command path is what
the log needs. That reasoning missed positional arguments: `api <method> <path>`
takes the path as an argument, so a document token moved from truncated to
recorded in a file that is kept for a week.

Apply both limits. Stop at the first flag, so a sensitive flag ahead of the
subcommand cannot slip through, and keep at most three words, so a positional
identifier after the command path cannot either. Removing either one fails a
test: the flag boundary alone lets the document token through, the word cap
alone lets --token=... through.

Verified case by case that nothing reaches the log that the pre-475f04a8
behaviour withheld.
The comment claimed the binary plus two words is the deepest command path in
this CLI. It is not: generated service commands go one level further, as
`drive file.comments create_v2` in the manifest tests shows, and the cap cuts
their last word. Calling the bound a measurement invites the next reader to
raise it for a command that does not fit — which would also admit the first
positional argument, where resource identifiers live.

State it as the privacy bound it is, and add the generated-command case to the
table so the trade-off is visible next to the cases it protects.
97e397c classified every startup failure, including the one the config
resolver had already classified. An unconfigured CLI comes back as
not_configured carrying "run: lark-cli config init"; wrapping it in
invalid_config put the wrong subtype in front — ProblemOf reads the outermost —
and dropped the hint entirely. A caller would be told the config is broken when
it was never written.

Pass typed errors through untouched and reserve a fresh error for the case
where the resolver gave none, where internal/unknown is the honest answer
rather than a guess at invalid_config.

Flag rejections now name the flag through WithParam, so a caller learns which
one to fix without reading the sentence, and the tests assert subtype and
param through ProblemOf instead of matching prose.
The comment sent readers to a follow-up in the pull request description that
does not exist there. Keep the reason in the source, where it is already
complete, and add the evidence that made the decision: applying the validator
moved four packages' expectations from /var to /private/var, because it
resolves symlinks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/im PR touches the im domain size/XL Architecture-level or global-impact change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant