Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6b5938bd8 | ||
|
|
4f95373420 | ||
|
|
6411d26a95 | ||
|
|
96b9cbce02 | ||
|
|
36877d00dc | ||
|
|
55d94d3b58 | ||
|
|
bfd0976b31 | ||
|
|
4a33e7e893 | ||
|
|
ee74765383 |
@@ -0,0 +1,8 @@
|
||||
---
|
||||
category: Changed
|
||||
---
|
||||
|
||||
- **Faster Schema Catalog assembly** — projects typed values into payload JSON
|
||||
without re-running a validation scan over documents `json.Marshal` has just
|
||||
produced, cutting roughly a third of the projection work across the full tool
|
||||
set. Untrusted JSON input keeps its existing validation.
|
||||
+63
-22
@@ -523,12 +523,22 @@ jobs:
|
||||
# shards at full-suite scope; release-scripts is included because its
|
||||
# dedicated job only runs at full-suite or release-sensitive scope, and
|
||||
# dropping it here would stop testing test/scripts changes entirely.
|
||||
# internal/app is carried by one shard per bounded partition rather than a
|
||||
# single app shard: the partitions used to run end to end inside one job,
|
||||
# where the Schema partition alone owned most of the wall clock. The
|
||||
# app-<partition> names are pinned to the helper's partition set by
|
||||
# TestCIAppRacePartitionMatrixMatchesHelper, so a partition can never lose
|
||||
# its job silently.
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard:
|
||||
- app
|
||||
- app-schema
|
||||
- app-a-b
|
||||
- app-c
|
||||
- app-d-r
|
||||
- app-s-z-example-fuzz
|
||||
- generators
|
||||
- helpers
|
||||
- cli
|
||||
@@ -570,9 +580,16 @@ jobs:
|
||||
TEST_SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Every app partition shard tests the same single internal/app
|
||||
# package, so the impacted-package query uses the base shard name and
|
||||
# the partition only selects which tests run.
|
||||
package_shard="$TEST_SHARD"
|
||||
case "$TEST_SHARD" in
|
||||
app-*) package_shard=app ;;
|
||||
esac
|
||||
package_output="$(
|
||||
./scripts/ci/changed-test-packages.sh \
|
||||
list-shard "$TEST_SHARD" "$TEST_BASE_REF" "$TEST_HEAD_REF"
|
||||
list-shard "$package_shard" "$TEST_BASE_REF" "$TEST_HEAD_REF"
|
||||
)"
|
||||
if [ -z "$package_output" ]; then
|
||||
echo "No buildable Go package in shard $TEST_SHARD is affected by this revision." \
|
||||
@@ -612,14 +629,19 @@ jobs:
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
if [ "$TEST_SHARD" = "app" ]; then
|
||||
# A single long-lived app test process retains every constructed
|
||||
# command tree in framework registries. Isolate Schema assembly and
|
||||
# bounded name ranges so each process releases that state on exit.
|
||||
test "${#packages[@]}" -eq 1
|
||||
./scripts/ci/run-app-race-tests.sh run "${packages[0]}"
|
||||
exit 0
|
||||
fi
|
||||
case "$TEST_SHARD" in
|
||||
app-*)
|
||||
# A single long-lived app test process retains every constructed
|
||||
# command tree in framework registries. Each partition is its own
|
||||
# job, so that state is released when the process exits and the
|
||||
# partitions run concurrently instead of end to end. The helper
|
||||
# still verifies that the partition patterns cover every top-level
|
||||
# test exactly once before running the one it was asked for.
|
||||
test "${#packages[@]}" -eq 1
|
||||
./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
if [ "$TEST_SHARD" = "release-scripts" ]; then
|
||||
# Mirror the dedicated release-contract job: these suites shell out
|
||||
# to archive tooling and are not race-instrumented there.
|
||||
@@ -640,14 +662,21 @@ jobs:
|
||||
needs: lint
|
||||
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && needs.lint.outputs.full_suite == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
# app runs several independently bounded processes; cli/smoke need headroom
|
||||
# beyond go test -timeout for setup + assembly.
|
||||
# internal/app is split across one shard per bounded partition so the
|
||||
# partitions run concurrently and each releases its framework registries
|
||||
# when the process exits; cli/smoke need headroom beyond go test -timeout for
|
||||
# setup + assembly. The app-<partition> names are pinned to the helper's
|
||||
# partition set by TestCIAppRacePartitionMatrixMatchesHelper.
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard:
|
||||
- app
|
||||
- app-schema
|
||||
- app-a-b
|
||||
- app-c
|
||||
- app-d-r
|
||||
- app-s-z-example-fuzz
|
||||
- generators
|
||||
- helpers
|
||||
- cli
|
||||
@@ -673,18 +702,30 @@ jobs:
|
||||
TEST_SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
package_output="$(./scripts/ci/test-packages.sh list "$TEST_SHARD")"
|
||||
# Every app partition shard tests the same single internal/app
|
||||
# package, so the package query uses the base shard name and the
|
||||
# partition only selects which tests run.
|
||||
package_shard="$TEST_SHARD"
|
||||
case "$TEST_SHARD" in
|
||||
app-*) package_shard=app ;;
|
||||
esac
|
||||
package_output="$(./scripts/ci/test-packages.sh list "$package_shard")"
|
||||
test -n "$package_output"
|
||||
mapfile -t packages <<< "$package_output"
|
||||
test "${#packages[@]}" -gt 0
|
||||
if [ "$TEST_SHARD" = "app" ]; then
|
||||
# A single long-lived app test process retains every constructed
|
||||
# command tree in framework registries. Isolate Schema assembly and
|
||||
# bounded name ranges so each process releases that state on exit.
|
||||
test "${#packages[@]}" -eq 1
|
||||
./scripts/ci/run-app-race-tests.sh run "${packages[0]}"
|
||||
exit 0
|
||||
fi
|
||||
case "$TEST_SHARD" in
|
||||
app-*)
|
||||
# A single long-lived app test process retains every constructed
|
||||
# command tree in framework registries. Each partition is its own
|
||||
# job, so that state is released when the process exits and the
|
||||
# partitions run concurrently instead of end to end. The helper
|
||||
# still verifies that the partition patterns cover every top-level
|
||||
# test exactly once before running the one it was asked for.
|
||||
test "${#packages[@]}" -eq 1
|
||||
./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}"
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
# cli/smoke own heavy NewRootCommand / Schema assembly under -race;
|
||||
# give them a dedicated package timeout on slower hosted runners.
|
||||
timeout_budget=12m
|
||||
|
||||
@@ -6,6 +6,37 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.59-beta.2] - 2026-08-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Privacy-safe CLI telemetry** (#1009) — reports reviewed command outcomes and profile identity dimensions while excluding command arguments, output, paths, device fingerprints, and automatic system dimensions; `DO_NOT_TRACK=1` disables reporting.
|
||||
|
||||
- **Feedback survey entry in root help** (#1019) — `dws --help` now closes with a Feedback section linking the user-experience survey form.
|
||||
|
||||
- **Wiki Shortcut workflows** — publishes 20 reviewed space, member, node, and
|
||||
activity shortcuts with strict collection validation, cursor handling,
|
||||
write-terminal evidence, safe read-backs where the backend supports them,
|
||||
task-oriented routing, and documented backend
|
||||
boundaries.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Chat IM ID flags** (#954) — standardizes chat command entry points on `--conversation-id` for conversation IDs and `--message-id` for message IDs, so help, Schema, and Agent recommendations use the same canonical flags.
|
||||
- **Legacy chat flag compatibility** (#954) — keeps older chat IM ID flags such as `--group`, `--id`, `--chat`, `--open-conversation-id`, `--msg-id`, and `--open-message-id` working as compatibility aliases where applicable, while hiding migrated aliases from recommended help and Schema surfaces.
|
||||
- **Chat group bots target flag** (#954) — keeps `dws chat group bots` on the visible `--group` flag; this command does not register `--group-name`, and `--group` accepts either an openConversationId or a uniquely resolved group name.
|
||||
|
||||
- **Faster Schema Catalog assembly** — projects typed values into payload JSON
|
||||
without re-running a validation scan over documents `json.Marshal` has just
|
||||
produced, cutting roughly a third of the projection work across the full tool
|
||||
set. Untrusted JSON input keeps its existing validation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Chat card update evidence** — distinguishes an accepted update request from an independently verified visible update, preserving the real `bizId` and warning callers not to repeat an unverified write.
|
||||
- **Chat command guidance** — splits message and group references by task and explains that `--from` is ambiguous between sender and time-range intent.
|
||||
|
||||
|
||||
## [1.0.59-beta.1] - 2026-08-14
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1127,21 +1127,37 @@ func putRawJSON(payload map[string]any, key string, raw json.RawMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// rawJSONValue decodes a JSON value that may come from an untrusted source, so
|
||||
// it validates before decoding. Callers holding output that json.Marshal just
|
||||
// produced should use typedJSONValue instead of paying the validation scan.
|
||||
func rawJSONValue(raw json.RawMessage) (any, error) {
|
||||
if !json.Valid(raw) {
|
||||
return nil, fmt.Errorf("invalid JSON value")
|
||||
}
|
||||
return decodeValidJSONValue(raw), nil
|
||||
}
|
||||
|
||||
// decodeValidJSONValue decodes JSON whose validity the caller has already
|
||||
// established, either by json.Valid or by having just marshaled it. Decode
|
||||
// errors are unreachable under that precondition and are therefore discarded,
|
||||
// exactly as this path behaved when the decode was inlined into rawJSONValue.
|
||||
func decodeValidJSONValue(raw json.RawMessage) any {
|
||||
decoder := json.NewDecoder(bytes.NewReader(raw))
|
||||
decoder.UseNumber()
|
||||
var value any
|
||||
_ = decoder.Decode(&value)
|
||||
return value, nil
|
||||
return value
|
||||
}
|
||||
|
||||
// typedJSONValue projects a typed value into the generic JSON shape the payload
|
||||
// renderers consume. json.Marshal output is valid by construction, so this path
|
||||
// decodes it directly: routing through rawJSONValue re-scanned every marshaled
|
||||
// document with json.Valid, which measured ~34% of Schema Catalog assembly time
|
||||
// across the 1121-tool set (26.0s -> 17.2s for the internal/app schema suite).
|
||||
func typedJSONValue(value any) (any, error) {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rawJSONValue(data)
|
||||
return decodeValidJSONValue(data), nil
|
||||
}
|
||||
|
||||
@@ -6,16 +6,35 @@ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
usage() {
|
||||
printf '%s\n' \
|
||||
"usage: $0 verify <app-package>" \
|
||||
" $0 run <app-package>" >&2
|
||||
" $0 run <app-package> [partition]" \
|
||||
" $0 list-partitions" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[ "$#" -eq 2 ] || usage
|
||||
mode="$1"
|
||||
app_package="$2"
|
||||
# Single source of truth for the partition set. CI runs one job per partition and
|
||||
# pins its shard names to this list, so a name that appears here without a
|
||||
# dispatch entry below fails closed rather than silently skipping tests.
|
||||
APP_PARTITIONS='schema a-b c d-r s-z-example-fuzz'
|
||||
|
||||
mode="${1:-}"
|
||||
partition=""
|
||||
case "$mode" in
|
||||
verify|run) ;;
|
||||
list-partitions)
|
||||
[ "$#" -eq 1 ] || usage
|
||||
for name in $APP_PARTITIONS; do
|
||||
printf '%s\n' "$name"
|
||||
done
|
||||
exit 0
|
||||
;;
|
||||
verify)
|
||||
[ "$#" -eq 2 ] || usage
|
||||
app_package="$2"
|
||||
;;
|
||||
run)
|
||||
[ "$#" -eq 2 ] || [ "$#" -eq 3 ] || usage
|
||||
app_package="$2"
|
||||
partition="${3:-}"
|
||||
;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
@@ -70,21 +89,49 @@ if [ "$unmatched_count" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for partition in \
|
||||
# The loop variable is deliberately not named "partition": that name holds the
|
||||
# partition requested on the command line, and run mode still executes this
|
||||
# discovery pass before dispatching.
|
||||
classified=''
|
||||
for spec in \
|
||||
"schema:$schema_count" \
|
||||
"a-b:$ab_count" \
|
||||
"c:$c_count" \
|
||||
"d-r:$dr_count" \
|
||||
"s-z-example-fuzz:$sz_count"
|
||||
do
|
||||
name="${partition%%:*}"
|
||||
count="${partition#*:}"
|
||||
name="${spec%%:*}"
|
||||
count="${spec#*:}"
|
||||
classified="$classified $name"
|
||||
if [ "$count" -eq 0 ]; then
|
||||
printf 'app race partition %s is empty\n' "$name" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The counters above and APP_PARTITIONS must describe the same set in both
|
||||
# directions. A counted partition missing from APP_PARTITIONS would never be
|
||||
# dispatched by any CI job, and a dispatchable partition with no counter would
|
||||
# escape the exact-coverage check above.
|
||||
for name in $APP_PARTITIONS; do
|
||||
case " $classified " in
|
||||
*" $name "*) ;;
|
||||
*)
|
||||
printf 'app partition %s has no coverage counter\n' "$name" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
for name in $classified; do
|
||||
case " $APP_PARTITIONS " in
|
||||
*" $name "*) ;;
|
||||
*)
|
||||
printf 'counted partition %s is not a dispatchable app partition\n' "$name" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
total_count="$(wc -l < "$tests" | tr -d ' ')"
|
||||
assigned_count=$((schema_count + ab_count + c_count + dr_count + sz_count))
|
||||
if [ "$assigned_count" -ne "$total_count" ]; then
|
||||
@@ -101,26 +148,73 @@ fi
|
||||
|
||||
run_partition() {
|
||||
name="$1"
|
||||
run_pattern="$2"
|
||||
skip_pattern="${3:-}"
|
||||
instrumentation="$2"
|
||||
run_pattern="$3"
|
||||
skip_pattern="${4:-}"
|
||||
|
||||
printf 'running internal/app race partition %s\n' "$name"
|
||||
# Fail closed on an unrecognized mode: a typo must not silently drop race
|
||||
# instrumentation from a partition that is supposed to carry it.
|
||||
case "$instrumentation" in
|
||||
race|no-race) ;;
|
||||
*)
|
||||
printf 'unknown instrumentation %s for app partition %s\n' \
|
||||
"$instrumentation" "$name" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
printf 'running internal/app %s partition %s\n' "$instrumentation" "$name"
|
||||
set -- -v -count=1 -timeout=15m -run "$run_pattern"
|
||||
if [ -n "$skip_pattern" ]; then
|
||||
go test -v -race -count=1 -timeout=15m \
|
||||
-run "$run_pattern" -skip "$skip_pattern" "$app_package"
|
||||
else
|
||||
go test -v -race -count=1 -timeout=15m \
|
||||
-run "$run_pattern" "$app_package"
|
||||
set -- "$@" -skip "$skip_pattern"
|
||||
fi
|
||||
if [ "$instrumentation" = race ]; then
|
||||
set -- -race "$@"
|
||||
fi
|
||||
go test "$@" "$app_package"
|
||||
}
|
||||
|
||||
# Schema assembly has the largest transient memory footprint. Run those tests
|
||||
# in a fresh process, then keep each remaining name range in its own process so
|
||||
# command trees retained by process-global registries are released between
|
||||
# partitions. The complementary run/skip patterns preserve the full test set.
|
||||
#
|
||||
# The schema partition runs uninstrumented. Its tests assert structural
|
||||
# Schema-to-Cobra contracts over a single goroutine: none of them call
|
||||
# t.Parallel or start a goroutine, so the race detector has no concurrent access
|
||||
# to observe here. The process-global lazy metadata that does need race coverage
|
||||
# (schema_source_root's atomic.Value, the parameter-binding lazy loaders) is
|
||||
# exercised by internal/cli's concurrent tests, which stay instrumented. The
|
||||
# instrumentation is not free on this partition: its shared sync.Once Catalog
|
||||
# build is allocation-heavy, and -race made the partition roughly 11x slower
|
||||
# (26s -> 291s locally, 357s in CI) without being able to report anything.
|
||||
schema_pattern='^Test.*Schema'
|
||||
run_partition schema "$schema_pattern"
|
||||
run_partition a-b '^Test[A-B]' "$schema_pattern"
|
||||
run_partition c '^TestC' "$schema_pattern"
|
||||
run_partition d-r '^Test[D-R]' "$schema_pattern"
|
||||
run_partition s-z-example-fuzz '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern"
|
||||
|
||||
# Dispatch table for the partition set declared in APP_PARTITIONS. CI passes one
|
||||
# partition per job so they run concurrently; running without a partition keeps
|
||||
# the original end-to-end behaviour for local use and for any caller that wants
|
||||
# the whole package in one invocation.
|
||||
run_named_partition() {
|
||||
case "$1" in
|
||||
schema) run_partition schema no-race "$schema_pattern" ;;
|
||||
a-b) run_partition a-b race '^Test[A-B]' "$schema_pattern" ;;
|
||||
c) run_partition c race '^TestC' "$schema_pattern" ;;
|
||||
d-r) run_partition d-r race '^Test[D-R]' "$schema_pattern" ;;
|
||||
s-z-example-fuzz)
|
||||
run_partition s-z-example-fuzz race '^(Test[S-Z]|Example|Fuzz)' "$schema_pattern"
|
||||
;;
|
||||
*)
|
||||
printf 'unknown app partition: %s\n' "$1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -n "$partition" ]; then
|
||||
run_named_partition "$partition"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for name in $APP_PARTITIONS; do
|
||||
run_named_partition "$name"
|
||||
done
|
||||
|
||||
@@ -1157,7 +1157,7 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
`test "$(git rev-parse HEAD^1)" = "$PR_BASE_SHA"`,
|
||||
`test "$(git rev-parse HEAD^2)" = "$PR_HEAD_SHA"`,
|
||||
`echo "TEST_HEAD_REF=$(git rev-parse HEAD)"`,
|
||||
`list-shard "$TEST_SHARD" "$TEST_BASE_REF" "$TEST_HEAD_REF"`,
|
||||
`list-shard "$package_shard" "$TEST_BASE_REF" "$TEST_HEAD_REF"`,
|
||||
"needs.lint.outputs.full_suite != 'true'",
|
||||
`name: "Test (race: ${{ matrix.shard }})"`,
|
||||
"name: Test (workflow and release contracts)",
|
||||
@@ -1219,16 +1219,20 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
}
|
||||
// The focused path fans the impacted set across the same shards as test-race
|
||||
// and runs each shard the way test-race runs it, so no single job carries
|
||||
// internal/app together with its reverse dependencies. internal/app keeps its
|
||||
// package-level headroom through the process-isolating helper instead of one
|
||||
// long -timeout, which is strictly stronger: every process releases the
|
||||
// framework registries it populated. release-scripts is asserted because its
|
||||
// dedicated job only runs at full-suite or release-sensitive scope, so losing
|
||||
// it here would silently stop testing test/scripts changes.
|
||||
// internal/app together with its reverse dependencies. internal/app is split
|
||||
// further into one shard per bounded partition, which keeps its package-level
|
||||
// headroom through the process-isolating helper instead of one long -timeout
|
||||
// and is strictly stronger than a single app job: every partition process
|
||||
// releases the framework registries it populated, and the partitions run
|
||||
// concurrently rather than end to end. Each partition shard still selects the
|
||||
// same single internal/app package, so the impacted-package query maps the
|
||||
// shard name back to app. release-scripts is asserted because its dedicated
|
||||
// job only runs at full-suite or release-sensitive scope, so losing it here
|
||||
// would silently stop testing test/scripts changes.
|
||||
for _, want := range []string{
|
||||
`if [ "$TEST_SHARD" = "app" ]; then`,
|
||||
`app-*) package_shard=app ;;`,
|
||||
`test "${#packages[@]}" -eq 1`,
|
||||
`./scripts/ci/run-app-race-tests.sh run "${packages[0]}"`,
|
||||
`./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}"`,
|
||||
`if [ "$TEST_SHARD" = "release-scripts" ]; then`,
|
||||
`go test -v -count=1 -timeout=10m "${packages[@]}"`,
|
||||
"timeout_budget=12m",
|
||||
@@ -1249,14 +1253,16 @@ func TestChangelogPRFastPathWorkflowContract(t *testing.T) {
|
||||
t.Fatal("Code Admission workflow missing race test job boundaries")
|
||||
}
|
||||
raceJob := admission[raceStart:raceEnd]
|
||||
// The app shard uses independently bounded test processes so process-global
|
||||
// command registries are released before the Schema assembly peak. Other full
|
||||
// race shards retain the dynamic package timeout: default/floor 12m, with
|
||||
// cli/smoke raised to 15m on slower hosted runners.
|
||||
// internal/app is carried by one shard per bounded partition, so process-global
|
||||
// command registries are released with each partition process and the Schema
|
||||
// assembly peak no longer sits in front of the other partitions. Each
|
||||
// partition shard resolves back to the same single internal/app package.
|
||||
// Other full race shards retain the dynamic package timeout: default/floor
|
||||
// 12m, with cli/smoke raised to 15m on slower hosted runners.
|
||||
for _, want := range []string{
|
||||
`if [ "$TEST_SHARD" = "app" ]; then`,
|
||||
`app-*) package_shard=app ;;`,
|
||||
`test "${#packages[@]}" -eq 1`,
|
||||
`./scripts/ci/run-app-race-tests.sh run "${packages[0]}"`,
|
||||
`./scripts/ci/run-app-race-tests.sh run "${packages[0]}" "${TEST_SHARD#app-}"`,
|
||||
"timeout_budget=12m",
|
||||
`if [ "$TEST_SHARD" = "cli" ] ||`,
|
||||
`[ "$TEST_SHARD" = "smoke" ]; then`,
|
||||
|
||||
@@ -99,6 +99,74 @@ func TestCIAppRacePartitionsCoverTopLevelTestsExactlyOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCIAppRacePartitionMatrixMatchesHelper pins the workflow's app partition
|
||||
// shards to the partition set the helper actually runs. The partitions are
|
||||
// separate CI jobs now, so the helper's own "covered exactly once" check can no
|
||||
// longer prove the whole package ran: a partition the helper knows about but no
|
||||
// matrix shard dispatches would silently stop running while every job stays
|
||||
// green. Both directions are asserted so a stale matrix shard fails too.
|
||||
func TestCIAppRacePartitionMatrixMatchesHelper(t *testing.T) {
|
||||
root := testPackagePlanRoot(t)
|
||||
script := filepath.Join(root, "scripts", "ci", "run-app-race-tests.sh")
|
||||
cmd := exec.Command("sh", script, "list-partitions")
|
||||
cmd.Dir = root
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%s list-partitions failed: %v\n%s", script, err, output)
|
||||
}
|
||||
partitions := strings.Fields(string(output))
|
||||
if len(partitions) == 0 {
|
||||
t.Fatalf("list-partitions returned no partitions: %q", output)
|
||||
}
|
||||
|
||||
workflow, err := os.ReadFile(filepath.Join(root, ".github", "workflows", "ci.yml"))
|
||||
if err != nil {
|
||||
t.Fatalf("read ci.yml: %v", err)
|
||||
}
|
||||
admission := string(workflow)
|
||||
|
||||
for _, job := range []struct {
|
||||
name string
|
||||
startMark string
|
||||
endMark string
|
||||
}{
|
||||
{"test-focused", "\n test-focused:\n", "\n test-race:\n"},
|
||||
{"test-race", "\n test-race:\n", "\n test-release-scripts:\n"},
|
||||
} {
|
||||
start := strings.Index(admission, job.startMark)
|
||||
end := strings.Index(admission, job.endMark)
|
||||
if start < 0 || end <= start {
|
||||
t.Fatalf("ci.yml is missing %s job boundaries", job.name)
|
||||
}
|
||||
body := admission[start:end]
|
||||
|
||||
for _, partition := range partitions {
|
||||
want := "- app-" + partition
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("%s matrix is missing shard %q for a partition the helper runs", job.name, want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(body, "\n") {
|
||||
shard := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(shard, "- app-") {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimPrefix(shard, "- app-")
|
||||
matched := false
|
||||
for _, partition := range partitions {
|
||||
if partition == name {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matched {
|
||||
t.Errorf("%s matrix shard %q has no matching helper partition", job.name, shard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCITestPackagePlanFailsClosedWhenGoListFails(t *testing.T) {
|
||||
root := testPackagePlanRoot(t)
|
||||
fakeBin := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user