@hasna/recordings 0.3.1 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -34
- package/bun.lock +96 -36
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
- package/dist/cli/index.js +197 -95
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/http/client.d.ts +0 -1
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.js +162 -73
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/release-install-policy.d.ts +21 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -1
- package/dist/mcp/index.js +144 -59
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +7 -3
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +4 -19
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +168 -110
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +144 -60
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +4 -3
- package/packaging/macos/build_release_pkg.sh +10 -4
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -0
- package/scripts/generate-sdk.ts +17 -16
- package/scripts/install_macos_app.sh +75 -28
- package/scripts/macos_artifact.ts +162 -45
- package/scripts/migrate.ts +2 -2
- package/scripts/release-suite-gate.ts +181 -0
- package/scripts/set-version.ts +5 -5
- package/scripts/smoke_macos_app.sh +80 -34
- package/scripts/vacuity-manifests/version-sites.tsv +8 -4
- package/src/native/Recordings/App/MenuBarStatusView.swift +9 -4
- package/src/native/Recordings/App/RecordingsApp.swift +37 -1
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
- package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +152 -18
- package/dist/lib/retired-deployment-modes.d.ts +0 -24
- package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Release-suite gate (wired into `prepublishOnly`).
|
|
4
|
+
*
|
|
5
|
+
* Runs the app's own Linux-CI-partitioned suite — `test:gated`, which covers
|
|
6
|
+
* every discovered `*.test.ts` (the quarantine file is empty by design) — and
|
|
7
|
+
* fails on ANY failure except the allowlisted pre-existing classes below.
|
|
8
|
+
*
|
|
9
|
+
* Why the allowlist exists: the suite is red at this release's base sha with
|
|
10
|
+
* exactly two measured pre-existing classes (both reproduced identically at
|
|
11
|
+
* the pristine base commit 4a9b31e56, classified non-blocking by the release
|
|
12
|
+
* lineage). The old `prepublishOnly` (`bun run test`, bare) could not pass on
|
|
13
|
+
* any box for independent reasons: no timeout flag for the long macOS
|
|
14
|
+
* lifecycle suites, and the ambient hosted-store environment on fleet boxes.
|
|
15
|
+
*
|
|
16
|
+
* The allowlist is SELF-INVALIDATING, the same property as
|
|
17
|
+
* `.github/linux-quarantine.txt`: if an allowlisted failure stops failing
|
|
18
|
+
* (the owning lane lands its fix, or a quarantined package ages out of the
|
|
19
|
+
* 7-day window), the gate exits 1 and demands the entry be removed. Any
|
|
20
|
+
* failure outside the allowlist fails the gate.
|
|
21
|
+
*
|
|
22
|
+
* The gate FAILS CLOSED on abnormal termination: a non-zero suite exit is
|
|
23
|
+
* acceptable only when it is fully explained by allowlisted failures — the
|
|
24
|
+
* runner completed (its summary line is present), every parsed failure is
|
|
25
|
+
* allowlisted, at least one allowlisted failure fired, and the runner's
|
|
26
|
+
* reported failure count equals the parsed count. A crash, signal kill,
|
|
27
|
+
* timeout, or runner error that produces a non-zero exit without a complete
|
|
28
|
+
* summary fails the gate (remediation cycle 2 of PR hasna/apps#826).
|
|
29
|
+
*/
|
|
30
|
+
import { spawnSync } from "node:child_process";
|
|
31
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
|
|
35
|
+
const SEP = "::";
|
|
36
|
+
|
|
37
|
+
const GATED_KEYS: Array<{ file: string; test: string; owner: string; reason: string }> = [
|
|
38
|
+
{
|
|
39
|
+
file: "src/__tests__/service-contract-manifest.test.ts",
|
|
40
|
+
test: "the repository satisfies the tracked contract kit's conformance checks",
|
|
41
|
+
owner: "contracts-surface lanes (hasna/apps PRs 743/749/754 lineage)",
|
|
42
|
+
reason:
|
|
43
|
+
"credential_seam_compliance flags the vendored client seam (resolveStorageClient in src/http/client.ts) and the RECORDINGS_API_KEY env read (src/lib/config.ts) - an in-flight migration owned by the contracts-surface lanes; classified pre-existing at the release base. Remove this entry when the lane lands.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
file: "src/__tests__/native-app-companion-contract.test.ts",
|
|
47
|
+
test: "(unnamed)",
|
|
48
|
+
owner: "release supply-chain policy",
|
|
49
|
+
reason:
|
|
50
|
+
"build_companion_cli.sh enforces --minimum-release-age=604800, so the companion build refuses @hasna/contracts@0.13.3 (published 2026-08-21T13:13Z) inside its 7-day quarantine by design until 2026-08-28. The npm publish path does not build the companion. Remove this entry when the package ages out.",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function run(argv: string[], env: Record<string, string>): { status: number; stdout: string; stderr: string } {
|
|
55
|
+
const res = spawnSync(process.execPath, argv, { encoding: "utf8", env: { ...process.env, ...env } });
|
|
56
|
+
return { status: res.status ?? -1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 1. Validate the partition (quarantine file must be consistent).
|
|
60
|
+
const check = run(["scripts/ci-linux-suite.ts", "--check"], {});
|
|
61
|
+
if (check.status !== 0) {
|
|
62
|
+
console.error("release-suite-gate: partition check failed:\n" + (check.stderr || check.stdout));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. Collect the gated file list.
|
|
67
|
+
const gated = run(["scripts/ci-linux-suite.ts", "--gated"], {});
|
|
68
|
+
if (gated.status !== 0) {
|
|
69
|
+
console.error("release-suite-gate: could not enumerate gated files:\n" + (gated.stderr || gated.stdout));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
const gatedFiles = gated.stdout.split("\n").filter(Boolean);
|
|
73
|
+
if (gatedFiles.length === 0) {
|
|
74
|
+
console.error("release-suite-gate: zero gated files - refusing to pass on an empty suite.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 3. Run the gated suite exactly like `test:gated`.
|
|
79
|
+
const logDir = mkdtempSync(join(tmpdir(), "recordings-release-gate-"));
|
|
80
|
+
const logPath = join(logDir, "suite.log");
|
|
81
|
+
const suite = spawnSync(
|
|
82
|
+
process.execPath,
|
|
83
|
+
["test", "--timeout", "120000", ...gatedFiles],
|
|
84
|
+
{
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
env: { ...process.env, RECORDINGS_TEST_TIMEOUT_MS: "120000" },
|
|
87
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
const output = (suite.stdout ?? "") + (suite.stderr ?? "");
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(logPath, output);
|
|
93
|
+
} catch {
|
|
94
|
+
/* log capture is best-effort */
|
|
95
|
+
}
|
|
96
|
+
const suiteStatus = suite.status ?? -1;
|
|
97
|
+
|
|
98
|
+
// 4. Parse failing tests as (file, description) pairs, in output order.
|
|
99
|
+
const failPairs: Array<{ file: string; test: string }> = [];
|
|
100
|
+
let currentFile = "";
|
|
101
|
+
for (const line of output.split("\n")) {
|
|
102
|
+
const fileMatch = /^(?:\.\/)?(src\/[^\s:]+\.test\.ts):$/.exec(line);
|
|
103
|
+
if (fileMatch) {
|
|
104
|
+
currentFile = fileMatch[1];
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const failMatch = /^\(fail\) (.+) \[\d+.*\]$/.exec(line);
|
|
108
|
+
if (failMatch) {
|
|
109
|
+
failPairs.push({ file: currentFile, test: failMatch[1].trim() });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 5. Classify.
|
|
114
|
+
const failuresByKey = new Map<string, number>();
|
|
115
|
+
for (const key of GATED_KEYS) {
|
|
116
|
+
failuresByKey.set(key.file + SEP + key.test, 0);
|
|
117
|
+
}
|
|
118
|
+
const unexpected: Array<{ file: string; test: string }> = [];
|
|
119
|
+
for (const pair of failPairs) {
|
|
120
|
+
const key = pair.file + SEP + pair.test;
|
|
121
|
+
if (failuresByKey.has(key)) {
|
|
122
|
+
failuresByKey.set(key, (failuresByKey.get(key) ?? 0) + 1);
|
|
123
|
+
} else {
|
|
124
|
+
unexpected.push(pair);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (unexpected.length > 0) {
|
|
129
|
+
console.error(
|
|
130
|
+
"release-suite-gate: FAIL - unexpected test failures:\n" +
|
|
131
|
+
unexpected.map((u) => " " + u.file + " :: " + u.test).join("\n"),
|
|
132
|
+
);
|
|
133
|
+
console.error("Full suite log: " + logPath);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 6. Fail closed on abnormal termination. A non-zero suite exit is only
|
|
138
|
+
// acceptable when it is fully explained by the allowlisted failures: the
|
|
139
|
+
// runner completed (summary line present), every parsed failure is
|
|
140
|
+
// allowlisted, at least one allowlisted failure fired, and the runner's
|
|
141
|
+
// reported failure count equals the parsed count (no unparsed failures).
|
|
142
|
+
// A crash, signal kill, timeout, or runner error produces a non-zero exit
|
|
143
|
+
// without a complete summary and must fail the gate.
|
|
144
|
+
if (suiteStatus !== 0) {
|
|
145
|
+
const summaryMatch = /Ran (\d+) tests across (\d+) files/.exec(output);
|
|
146
|
+
// The tally is "N pass[,/ ] M fail" in the runner's closing summary. The
|
|
147
|
+
// LAST match is taken (bun prints the tally once, but a naive first match
|
|
148
|
+
// can hit "0.13.3 failed to resolve" -> "3 fail" inside a version string).
|
|
149
|
+
const tallyMatches = [...output.matchAll(/(\d+) pass[\s,/]+(\d+) fail/g)];
|
|
150
|
+
const failCountMatch = tallyMatches.length > 0 ? tallyMatches[tallyMatches.length - 1] : null;
|
|
151
|
+
const reportedFails = failCountMatch ? Number(failCountMatch[2]) : null;
|
|
152
|
+
const anyAllowlistedFired = failPairs.length > 0;
|
|
153
|
+
const complete = summaryMatch !== null && reportedFails !== null && reportedFails === failPairs.length;
|
|
154
|
+
if (!anyAllowlistedFired || !complete) {
|
|
155
|
+
console.error(
|
|
156
|
+
"release-suite-gate: FAIL - suite exited " + suiteStatus +
|
|
157
|
+
" with " + failPairs.length + " parsed failure(s) but the run is incomplete or unexplained:" +
|
|
158
|
+
"\n summary line present: " + (summaryMatch !== null) +
|
|
159
|
+
"\n reported failure count: " + String(reportedFails) +
|
|
160
|
+
"\n parsed failure count: " + failPairs.length,
|
|
161
|
+
);
|
|
162
|
+
console.error("Full suite log: " + logPath);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 7. Self-invalidation: an allowlisted failure that no longer fails must be removed.
|
|
168
|
+
const stale = GATED_KEYS.filter((key) => (failuresByKey.get(key.file + SEP + key.test) ?? 0) === 0);
|
|
169
|
+
if (stale.length > 0) {
|
|
170
|
+
console.error(
|
|
171
|
+
"release-suite-gate: FAIL - allowlisted failure no longer fails; remove the entry from scripts/release-suite-gate.ts:\n" +
|
|
172
|
+
stale.map((s) => " " + s.file + " :: " + s.test + " - " + s.owner).join("\n"),
|
|
173
|
+
);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(
|
|
178
|
+
"release-suite-gate: PASS - " + failPairs.length + " classified pre-existing failure(s) allowlisted:" +
|
|
179
|
+
failPairs.map((p) => "\n " + p.file + " :: " + p.test).join(""),
|
|
180
|
+
);
|
|
181
|
+
process.exit(0);
|
package/scripts/set-version.ts
CHANGED
|
@@ -16,17 +16,17 @@ export type VersionSite = {
|
|
|
16
16
|
pattern: RegExp;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// src/version.ts is deliberately NOT a site: it derives VERSION from
|
|
20
|
+
// package.json at import time (see src/version.ts), so it holds no literal
|
|
21
|
+
// this writer could keep in sync, and a monorepo changesets bump can no
|
|
22
|
+
// longer leave it stale. The remaining sites are the native bundle surfaces,
|
|
23
|
+
// which cannot derive and must be written here.
|
|
19
24
|
export const versionSites: VersionSite[] = [
|
|
20
25
|
{
|
|
21
26
|
file: "package.json",
|
|
22
27
|
label: "package.json version",
|
|
23
28
|
pattern: /("version"\s*:\s*")([^"]+)(")/,
|
|
24
29
|
},
|
|
25
|
-
{
|
|
26
|
-
file: "src/version.ts",
|
|
27
|
-
label: "src/version.ts VERSION",
|
|
28
|
-
pattern: /(export const VERSION = ")([^"]+)(")/,
|
|
29
|
-
},
|
|
30
30
|
{
|
|
31
31
|
file: "src/native/Recordings/RecordingsLib/Info.plist",
|
|
32
32
|
label: "Info.plist CFBundleShortVersionString",
|
|
@@ -2,10 +2,26 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
umask 077
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
SMOKE_VARIANT="full"
|
|
6
|
+
if [ "$#" -ge 3 ]; then
|
|
7
|
+
case "$3" in
|
|
8
|
+
--variant)
|
|
9
|
+
SMOKE_VARIANT="${4:-full}"
|
|
10
|
+
;;
|
|
11
|
+
*)
|
|
12
|
+
echo "Usage: $0 <HasnaRecordings.app> <explicit absolute Bun executable path> [--variant full|bar]" >&2
|
|
13
|
+
exit 2
|
|
14
|
+
;;
|
|
15
|
+
esac
|
|
16
|
+
case "$SMOKE_VARIANT" in
|
|
17
|
+
full|bar) ;;
|
|
18
|
+
*)
|
|
19
|
+
echo "SMOKE_VARIANT must be full or bar; got: $SMOKE_VARIANT" >&2
|
|
20
|
+
exit 2
|
|
21
|
+
;;
|
|
22
|
+
esac
|
|
8
23
|
fi
|
|
24
|
+
readonly SMOKE_VARIANT
|
|
9
25
|
|
|
10
26
|
HOST_UNAME_EXECUTABLE="/usr/bin/uname"
|
|
11
27
|
SYSTEM_DIRNAME_EXECUTABLE="/usr/bin/dirname"
|
|
@@ -35,7 +51,7 @@ HOST_PLATFORM="$("$HOST_UNAME_EXECUTABLE" -s)"
|
|
|
35
51
|
readonly HOST_PLATFORM
|
|
36
52
|
|
|
37
53
|
if [ "$HOST_PLATFORM" != "Darwin" ] && [ "${RECORDINGS_TEST_SMOKE_ALLOW_NON_DARWIN:-0}" != "1" ]; then
|
|
38
|
-
echo "
|
|
54
|
+
echo "HasnaRecordings.app runtime smoke is only supported on macOS." >&2
|
|
39
55
|
exit 1
|
|
40
56
|
fi
|
|
41
57
|
|
|
@@ -107,12 +123,12 @@ HOME_DIRECTORY="${HOME:-}"
|
|
|
107
123
|
case "$HOME_DIRECTORY" in
|
|
108
124
|
/*) ;;
|
|
109
125
|
*)
|
|
110
|
-
echo "
|
|
126
|
+
echo "HasnaRecordings.app runtime smoke requires an absolute HOME path." >&2
|
|
111
127
|
exit 1
|
|
112
128
|
;;
|
|
113
129
|
esac
|
|
114
130
|
if [ ! -d "$HOME_DIRECTORY" ]; then
|
|
115
|
-
echo "
|
|
131
|
+
echo "HasnaRecordings.app runtime smoke HOME is not a directory: $HOME_DIRECTORY" >&2
|
|
116
132
|
exit 1
|
|
117
133
|
fi
|
|
118
134
|
|
|
@@ -120,7 +136,7 @@ APP_PARENT="$(cd -P "$("$DIRNAME_EXECUTABLE" "$1")" && "$PWD_EXECUTABLE" -P)"
|
|
|
120
136
|
APP_PATH="$APP_PARENT/$("$BASENAME_EXECUTABLE" "$1")"
|
|
121
137
|
EXECUTABLE="$APP_PATH/Contents/MacOS/Recordings"
|
|
122
138
|
if [ ! -x "$EXECUTABLE" ]; then
|
|
123
|
-
echo "
|
|
139
|
+
echo "HasnaRecordings.app runtime smoke executable is missing: $EXECUTABLE" >&2
|
|
124
140
|
exit 1
|
|
125
141
|
fi
|
|
126
142
|
|
|
@@ -301,7 +317,7 @@ cleanup() {
|
|
|
301
317
|
fi
|
|
302
318
|
if [ -n "$SMOKE_APP_PID" ] && [ -n "$SMOKE_APP_PID_START_IDENTITY" ]; then
|
|
303
319
|
terminate_verified_process \
|
|
304
|
-
"
|
|
320
|
+
"HasnaRecordings.app" \
|
|
305
321
|
"$SMOKE_APP_PID" \
|
|
306
322
|
"$EXECUTABLE" \
|
|
307
323
|
"$SMOKE_APP_PID_START_IDENTITY" || true
|
|
@@ -345,6 +361,12 @@ run_smoke() {
|
|
|
345
361
|
if [ "$mode" = "permission-helper" ]; then
|
|
346
362
|
arguments=(--request-permissions "${arguments[@]}")
|
|
347
363
|
fi
|
|
364
|
+
if [ "$SMOKE_VARIANT" = "bar" ]; then
|
|
365
|
+
# Bar builds are bar-only by construction (compile-time RECORDINGS_BAR_ONLY); the
|
|
366
|
+
# explicit argument is a belt-and-suspenders self-describing launch record so the
|
|
367
|
+
# launch is attributable to the variant even without the build define.
|
|
368
|
+
arguments=(--bar-only "${arguments[@]}")
|
|
369
|
+
fi
|
|
348
370
|
|
|
349
371
|
SMOKE_PID=""
|
|
350
372
|
SMOKE_PID_START_IDENTITY=""
|
|
@@ -355,7 +377,7 @@ run_smoke() {
|
|
|
355
377
|
if ! SMOKE_APP_IDENTITIES_BEFORE_LAUNCH="$(
|
|
356
378
|
process_records_for_exact_executable "$EXECUTABLE"
|
|
357
379
|
)"; then
|
|
358
|
-
echo "
|
|
380
|
+
echo "HasnaRecordings.app runtime smoke ${mode} could not inventory exact app processes before launch." >&2
|
|
359
381
|
return 1
|
|
360
382
|
fi
|
|
361
383
|
|
|
@@ -377,24 +399,24 @@ run_smoke() {
|
|
|
377
399
|
result_pid="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
|
|
378
400
|
"$BUN_EXECUTABLE" -e 'const value = await Bun.file(process.argv[1]).json(); console.log(value.processIdentifier)' "$output")"
|
|
379
401
|
if ! [[ "$result_pid" =~ ^[1-9][0-9]*$ ]] || ! kill -0 "$result_pid" 2>/dev/null; then
|
|
380
|
-
echo "
|
|
402
|
+
echo "HasnaRecordings.app runtime smoke ${mode} reported a process that is not running." >&2
|
|
381
403
|
return 1
|
|
382
404
|
fi
|
|
383
405
|
if ! result_pid_start_identity="$(
|
|
384
406
|
capture_process_start_identity "$result_pid" "$EXECUTABLE"
|
|
385
407
|
)"; then
|
|
386
|
-
echo "
|
|
408
|
+
echo "HasnaRecordings.app runtime smoke ${mode} reported a process outside the exact app path." >&2
|
|
387
409
|
return 1
|
|
388
410
|
fi
|
|
389
411
|
if process_identity_existed_before_launch "$result_pid" "$result_pid_start_identity"; then
|
|
390
|
-
echo "
|
|
412
|
+
echo "HasnaRecordings.app runtime smoke ${mode} reported an app process that predated this launch." >&2
|
|
391
413
|
return 1
|
|
392
414
|
fi
|
|
393
415
|
if [ -n "$SMOKE_APP_PID" ] && {
|
|
394
416
|
[ "$result_pid" != "$SMOKE_APP_PID" ] ||
|
|
395
417
|
[ "$result_pid_start_identity" != "$SMOKE_APP_PID_START_IDENTITY" ];
|
|
396
418
|
}; then
|
|
397
|
-
echo "
|
|
419
|
+
echo "HasnaRecordings.app runtime smoke ${mode} evidence did not match the exact launched app process." >&2
|
|
398
420
|
return 1
|
|
399
421
|
fi
|
|
400
422
|
SMOKE_APP_PID="$result_pid"
|
|
@@ -407,19 +429,19 @@ run_smoke() {
|
|
|
407
429
|
break
|
|
408
430
|
fi
|
|
409
431
|
if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
|
|
410
|
-
echo "
|
|
432
|
+
echo "HasnaRecordings.app runtime smoke ${mode} exited without evidence." >&2
|
|
411
433
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
412
434
|
return 1
|
|
413
435
|
fi
|
|
414
436
|
"$SLEEP_EXECUTABLE" 0.1
|
|
415
437
|
done
|
|
416
438
|
if [ ! -f "$output" ]; then
|
|
417
|
-
echo "
|
|
439
|
+
echo "HasnaRecordings.app runtime smoke ${mode} timed out." >&2
|
|
418
440
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
419
441
|
return 1
|
|
420
442
|
fi
|
|
421
443
|
if [ -z "$SMOKE_APP_PID" ]; then
|
|
422
|
-
echo "
|
|
444
|
+
echo "HasnaRecordings.app runtime smoke ${mode} did not bind evidence to the exact app process." >&2
|
|
423
445
|
return 1
|
|
424
446
|
fi
|
|
425
447
|
|
|
@@ -428,6 +450,7 @@ run_smoke() {
|
|
|
428
450
|
PATH="$SANITIZED_PATH" \
|
|
429
451
|
TMPDIR="$WORK_DIR" \
|
|
430
452
|
SMOKE_MODE="$mode" \
|
|
453
|
+
SMOKE_VARIANT="$SMOKE_VARIANT" \
|
|
431
454
|
SMOKE_FOCUS_EVIDENCE="$SMOKE_FOCUS_EVIDENCE" \
|
|
432
455
|
EXPECTED_HELPER="$APP_PATH/Contents/Helpers/recordings" \
|
|
433
456
|
"$BUN_EXECUTABLE" -e '
|
|
@@ -454,19 +477,38 @@ run_smoke() {
|
|
|
454
477
|
if (JSON.stringify(result.renderedStatusLabels) !== JSON.stringify(expected)) {
|
|
455
478
|
fail(`unexpected status labels ${JSON.stringify(result.renderedStatusLabels)}`);
|
|
456
479
|
}
|
|
457
|
-
if (
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
480
|
+
if (process.env.SMOKE_VARIANT === "bar") {
|
|
481
|
+
// The bar variant launches windowless by construction: no workspace window is
|
|
482
|
+
// ever created, activated, or reused, the app stays an accessory (LSUIElement),
|
|
483
|
+
// and no window is exposed. This is the runtime half of the P1 #2 regression —
|
|
484
|
+
// the full-build smoke below must keep creating the window.
|
|
485
|
+
if (result.windowCreationCount !== 0 || result.windowActivationCount !== 0) {
|
|
486
|
+
fail("bar-only launch created or activated a workspace window");
|
|
487
|
+
}
|
|
488
|
+
if (result.retainedWindowReused !== false) {
|
|
489
|
+
fail("bar-only launch reused a workspace window");
|
|
490
|
+
}
|
|
491
|
+
if (result.mainWindowIsVisible || result.mainWindowCanBecomeKey || result.mainWindowIsKey) {
|
|
492
|
+
fail("bar-only launch exposed a main window");
|
|
493
|
+
}
|
|
494
|
+
if (result.applicationActivationPolicy !== 1) {
|
|
495
|
+
fail("bar-only launch did not stay an accessory activation policy");
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
if (result.windowCreationCount !== 1 || result.windowActivationCount !== 2) {
|
|
499
|
+
fail("retained-window activation path was not exercised twice");
|
|
500
|
+
}
|
|
501
|
+
if (result.retainedWindowReused !== true) fail("main window was not retained");
|
|
502
|
+
if (result.applicationActivationPolicy !== 0) fail("main window did not set regular activation policy");
|
|
503
|
+
if (!result.mainWindowIsVisible || !result.mainWindowCanBecomeKey) {
|
|
504
|
+
fail("retained main window was not visible and capable of becoming key");
|
|
505
|
+
}
|
|
506
|
+
if (!result.applicationIsActive || !result.mainWindowIsKey) {
|
|
507
|
+
if (process.env.SMOKE_FOCUS_EVIDENCE !== "ssh-unavailable") {
|
|
508
|
+
fail("Open Recordings did not make the retained window active and key");
|
|
509
|
+
}
|
|
510
|
+
focusEvidenceStatus = "ssh-unavailable";
|
|
468
511
|
}
|
|
469
|
-
focusEvidenceStatus = "ssh-unavailable";
|
|
470
512
|
}
|
|
471
513
|
} else if (mode === "resolver") {
|
|
472
514
|
if (result.menuBarSurfaceCount !== 0 || result.globalHandlersInstalled !== false) {
|
|
@@ -504,7 +546,7 @@ run_smoke() {
|
|
|
504
546
|
' "$output"
|
|
505
547
|
|
|
506
548
|
if [ -e "$completion" ]; then
|
|
507
|
-
echo "
|
|
549
|
+
echo "HasnaRecordings.app runtime smoke ${mode} produced completion before the challenge." >&2
|
|
508
550
|
return 1
|
|
509
551
|
fi
|
|
510
552
|
challenge="$("$ENV_EXECUTABLE" -i HOME="$HOME_DIRECTORY" PATH="$SANITIZED_PATH" TMPDIR="$WORK_DIR" \
|
|
@@ -512,7 +554,7 @@ run_smoke() {
|
|
|
512
554
|
if ! rechecked_start_identity="$(
|
|
513
555
|
capture_process_start_identity "$result_pid" "$EXECUTABLE"
|
|
514
556
|
)" || [ "$rechecked_start_identity" != "$result_pid_start_identity" ]; then
|
|
515
|
-
echo "
|
|
557
|
+
echo "HasnaRecordings.app runtime smoke ${mode} process identity changed before completion challenge." >&2
|
|
516
558
|
return 1
|
|
517
559
|
fi
|
|
518
560
|
write_atomic_text_file "$acknowledgement" "$challenge"
|
|
@@ -522,7 +564,7 @@ run_smoke() {
|
|
|
522
564
|
"$SLEEP_EXECUTABLE" 0.1
|
|
523
565
|
done
|
|
524
566
|
if kill -0 "$SMOKE_PID" 2>/dev/null; then
|
|
525
|
-
echo "
|
|
567
|
+
echo "HasnaRecordings.app runtime smoke ${mode} completion handshake timed out." >&2
|
|
526
568
|
return 1
|
|
527
569
|
fi
|
|
528
570
|
if wait "$SMOKE_PID" 2>/dev/null; then
|
|
@@ -535,7 +577,7 @@ run_smoke() {
|
|
|
535
577
|
SMOKE_APP_PID=""
|
|
536
578
|
SMOKE_APP_PID_START_IDENTITY=""
|
|
537
579
|
if [ "$wrapper_status" -ne 0 ]; then
|
|
538
|
-
echo "
|
|
580
|
+
echo "HasnaRecordings.app runtime smoke ${mode} open -W wrapper exited unsuccessfully (${wrapper_status})." >&2
|
|
539
581
|
"$SED_EXECUTABLE" -n '1,120p' "$log" >&2
|
|
540
582
|
return 1
|
|
541
583
|
fi
|
|
@@ -560,7 +602,7 @@ run_smoke() {
|
|
|
560
602
|
process.exit(1);
|
|
561
603
|
}
|
|
562
604
|
' "$completion"; then
|
|
563
|
-
echo "
|
|
605
|
+
echo "HasnaRecordings.app runtime smoke ${mode} did not provide a valid completion response." >&2
|
|
564
606
|
return 1
|
|
565
607
|
fi
|
|
566
608
|
|
|
@@ -570,4 +612,8 @@ run_smoke() {
|
|
|
570
612
|
run_smoke normal
|
|
571
613
|
run_smoke permission-helper
|
|
572
614
|
run_smoke resolver
|
|
573
|
-
|
|
615
|
+
if [ "$SMOKE_VARIANT" = "bar" ]; then
|
|
616
|
+
echo "HasnaRecordings.app runtime smoke passed (bar variant): menu bar, windowless launch, helper isolation, and packaged resolver."
|
|
617
|
+
else
|
|
618
|
+
echo "HasnaRecordings.app runtime smoke passed: menu bar, retained window, helper isolation, and packaged resolver."
|
|
619
|
+
fi
|
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
# manifest is not wired into a package.json aggregate script.
|
|
16
16
|
#
|
|
17
17
|
# --- every version site agrees with package.json. Models 8de7068: one site bumped, the rest not.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
# I38-00553 (2026-08-21): src/version.ts now DERIVES from package.json and holds no literal, so it
|
|
19
|
+
# is no longer a writer site (see set-version.ts) and cannot be mutated here. The residual drift
|
|
20
|
+
# class is the mirror direction: the package.json authority corrupted while the native sites stay
|
|
21
|
+
# put. Rows below were re-needled to the 0.3.5 tree.
|
|
22
|
+
delete src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares package.json 0.3.5 1
|
|
23
|
+
duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.3.5 1
|
|
24
|
+
duplicate src/__tests__/version-site-guard.test.ts agree in the working tree, on the version package.json declares src/native/Recordings/RecordingsLib/Info.plist 0.3.5 2
|
|
21
25
|
# --- a stale site is reported, not silently accepted: the mismatch filter removed.
|
|
22
26
|
delete src/__tests__/version-site-guard.test.ts a single stale site is reported rather than silently accepted scripts/set-version.ts .filter((reading) => reading.version !== expected) 1
|
|
23
27
|
# --- applyVersion writes every site in one pass: the per-file grouping removed.
|
|
@@ -58,4 +62,4 @@ delete src/__tests__/version-site-guard.test.ts applyVersion rewrites every site
|
|
|
58
62
|
#
|
|
59
63
|
# The generated SDK's version stamp -- a fifth copy of the release version that `versionSites`
|
|
60
64
|
# deliberately does not write -- goes stale again.
|
|
61
|
-
delete src/__tests__/version-site-guard.test.ts the generated SDK header carries the version package.json declares src/sdk/v1.generated.ts .
|
|
65
|
+
delete src/__tests__/version-site-guard.test.ts the generated SDK header carries the version package.json declares src/sdk/v1.generated.ts 0.3.5 1
|
|
@@ -24,6 +24,9 @@ struct MenuBarStatusLabel: View {
|
|
|
24
24
|
struct MenuBarStatusView: View {
|
|
25
25
|
@ObservedObject var store: RecordingsStore
|
|
26
26
|
let openRecordings: () -> Void
|
|
27
|
+
/// Bar-only builds have no workspace window to open; the entry point is hidden so
|
|
28
|
+
/// the affordance does not exist where the surface cannot exist.
|
|
29
|
+
let barOnly: Bool
|
|
27
30
|
|
|
28
31
|
var body: some View {
|
|
29
32
|
VStack(alignment: .leading, spacing: 12) {
|
|
@@ -50,11 +53,13 @@ struct MenuBarStatusView: View {
|
|
|
50
53
|
|
|
51
54
|
Divider()
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if !barOnly {
|
|
57
|
+
Button(action: openRecordings) {
|
|
58
|
+
Label("Open Recordings", systemImage: "macwindow")
|
|
59
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
60
|
+
}
|
|
61
|
+
.buttonStyle(.plain)
|
|
56
62
|
}
|
|
57
|
-
.buttonStyle(.plain)
|
|
58
63
|
|
|
59
64
|
SettingsLink {
|
|
60
65
|
Label("Settings", systemImage: "gearshape")
|
|
@@ -56,6 +56,8 @@ final class RecordingsAppDelegate: NSObject, NSApplicationDelegate {
|
|
|
56
56
|
final class RecordingsAppState: ObservableObject {
|
|
57
57
|
let store: RecordingsStore?
|
|
58
58
|
let declaresMenuBar: Bool
|
|
59
|
+
let barOnly: Bool
|
|
60
|
+
let declaresWindow: Bool
|
|
59
61
|
let runtimeSmokeProbe: RuntimeSmokeProbe?
|
|
60
62
|
private let runtimeSmokeMode: String?
|
|
61
63
|
private let runtimeSmokeOutputPath: String?
|
|
@@ -67,6 +69,8 @@ final class RecordingsAppState: ObservableObject {
|
|
|
67
69
|
|
|
68
70
|
init(plan: PermissionRequestLaunchPlan) {
|
|
69
71
|
declaresMenuBar = plan.declaresMenuBar
|
|
72
|
+
barOnly = plan.isBarOnly
|
|
73
|
+
declaresWindow = plan.declaresWindow
|
|
70
74
|
runtimeSmokeMode = plan.runtimeSmokeMode
|
|
71
75
|
runtimeSmokeOutputPath = plan.runtimeSmokeOutputPath
|
|
72
76
|
runtimeSmokeAcknowledgementPath = plan.runtimeSmokeAcknowledgementPath
|
|
@@ -86,6 +90,14 @@ final class RecordingsAppState: ObservableObject {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
func openRecordings() {
|
|
93
|
+
// Bar launches never create the workspace window — on any path, including the
|
|
94
|
+
// reopen handler and the runtime smoke. Keyed on declaresWindow (NOT on
|
|
95
|
+
// declaresMainWindow, which excludes every runtime smoke): a full build's
|
|
96
|
+
// normal-mode smoke must keep exercising the window, and only bar builds finish
|
|
97
|
+
// windowless. This was the cycle-2 P1 of the terminated PR #269 lineage — a
|
|
98
|
+
// windowless branch keyed on declaresMainWindow made the FULL smoke fail
|
|
99
|
+
// deterministically on every build.
|
|
100
|
+
guard declaresWindow else { return }
|
|
89
101
|
if let store {
|
|
90
102
|
showWindow(contentView: NSHostingView(rootView: ContentView(store: store)))
|
|
91
103
|
} else if runtimeSmokeMode == "normal" {
|
|
@@ -181,6 +193,23 @@ final class RecordingsAppState: ObservableObject {
|
|
|
181
193
|
runtimeSmokeProbe.completed = { [weak self, weak runtimeSmokeProbe] in
|
|
182
194
|
guard let self, let runtimeSmokeProbe else { return }
|
|
183
195
|
let accessibility = RuntimeSmokeAccessibilitySnapshot.processMenuBarExtras()
|
|
196
|
+
if self.barOnly {
|
|
197
|
+
// Windowless by construction: the bar smoke must never create the
|
|
198
|
+
// workspace window, and it reports the windowless state directly. This
|
|
199
|
+
// branch keys on barOnly, NOT on declaresMainWindow — that property
|
|
200
|
+
// excludes every runtime smoke and made the FULL build's smoke fail
|
|
201
|
+
// deterministically in the terminated PR #269 lineage (cycle-2 P1). A
|
|
202
|
+
// nil-window comparison would also read as a retained window (nil ===
|
|
203
|
+
// nil), so the windowless result is finished here explicitly.
|
|
204
|
+
self.finishRuntimeSmoke(
|
|
205
|
+
mode: mode,
|
|
206
|
+
surfaceCount: runtimeSmokeProbe.surfaceAppearances,
|
|
207
|
+
labels: runtimeSmokeProbe.renderedLabels,
|
|
208
|
+
accessibility: accessibility,
|
|
209
|
+
retainedWindowReused: false
|
|
210
|
+
)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
184
213
|
self.openRecordings()
|
|
185
214
|
let firstWindow = self.mainWindow
|
|
186
215
|
self.openRecordings()
|
|
@@ -202,6 +231,9 @@ final class RecordingsAppState: ObservableObject {
|
|
|
202
231
|
retainedWindowReused: Bool,
|
|
203
232
|
attempt: Int = 0
|
|
204
233
|
) {
|
|
234
|
+
// Only the non-bar smoke reaches this (bar launches finish windowless in the
|
|
235
|
+
// probe completion handler above), so the settled state is exactly the retained
|
|
236
|
+
// window being key, unchanged from the base contract.
|
|
205
237
|
let windowSettled = NSApplication.shared.isActive && (mainWindow?.isKeyWindow ?? false)
|
|
206
238
|
guard windowSettled || attempt >= 60 else {
|
|
207
239
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
|
@@ -316,7 +348,11 @@ struct RecordingsApp: App {
|
|
|
316
348
|
@SceneBuilder var body: some Scene {
|
|
317
349
|
MenuBarExtra(isInserted: menuBarInsertion) {
|
|
318
350
|
if let store = state.store {
|
|
319
|
-
MenuBarStatusView(
|
|
351
|
+
MenuBarStatusView(
|
|
352
|
+
store: store,
|
|
353
|
+
openRecordings: state.openRecordings,
|
|
354
|
+
barOnly: state.barOnly
|
|
355
|
+
)
|
|
320
356
|
} else if state.runtimeSmokeProbe != nil {
|
|
321
357
|
EmptyView()
|
|
322
358
|
}
|
|
@@ -11,17 +11,17 @@
|
|
|
11
11
|
<key>CFBundleName</key>
|
|
12
12
|
<string>Recordings</string>
|
|
13
13
|
<key>CFBundleDisplayName</key>
|
|
14
|
-
<string>Recordings</string>
|
|
14
|
+
<string>Hasna Recordings</string>
|
|
15
15
|
<key>CFBundleExecutable</key>
|
|
16
16
|
<string>Recordings</string>
|
|
17
17
|
<key>CFBundleIdentifier</key>
|
|
18
18
|
<string>com.hasna.recordings</string>
|
|
19
19
|
<key>CFBundleVersion</key>
|
|
20
|
-
<string>0.3.
|
|
20
|
+
<string>0.3.8</string>
|
|
21
21
|
<key>CFBundlePackageType</key>
|
|
22
22
|
<string>APPL</string>
|
|
23
23
|
<key>CFBundleShortVersionString</key>
|
|
24
|
-
<string>0.3.
|
|
24
|
+
<string>0.3.8</string>
|
|
25
25
|
<key>LSMinimumSystemVersion</key>
|
|
26
26
|
<string>26.0</string>
|
|
27
27
|
<key>NSMicrophoneUsageDescription</key>
|
|
@@ -7,10 +7,19 @@ public struct PermissionRequestLaunchPlan: Sendable, Equatable {
|
|
|
7
7
|
public let runtimeSmokeOutputPath: String?
|
|
8
8
|
public let runtimeSmokeAcknowledgementPath: String?
|
|
9
9
|
public let runtimeSmokeCompletionPath: String?
|
|
10
|
+
public let isBarOnly: Bool
|
|
10
11
|
|
|
11
12
|
public var isRuntimeSmoke: Bool { runtimeSmokeMode != nil }
|
|
12
13
|
public var installsGlobalHandlers: Bool { !isHelper && !isRuntimeSmoke }
|
|
13
|
-
|
|
14
|
+
/// The workspace window may be created only when this holds: never for the permission
|
|
15
|
+
/// helper, never for a bar launch, and regardless of runtime-smoke mode — a full
|
|
16
|
+
/// build's smoke keeps exercising the window deterministically, and only bar builds
|
|
17
|
+
/// finish windowless.
|
|
18
|
+
public var declaresWindow: Bool { !isHelper && !isBarOnly }
|
|
19
|
+
/// Controls the init auto-open and the reopen handler ONLY. Runtime smoke launches
|
|
20
|
+
/// are excluded so the smoke controls window creation deterministically, and bar
|
|
21
|
+
/// launches are excluded so a real bar launch never auto-opens the workspace window.
|
|
22
|
+
public var declaresMainWindow: Bool { !isHelper && !isRuntimeSmoke && !isBarOnly }
|
|
14
23
|
public var declaresMenuBar: Bool {
|
|
15
24
|
if isRuntimeSmoke { return runtimeSmokeMode == "normal" }
|
|
16
25
|
return !isHelper
|
|
@@ -31,6 +40,17 @@ public struct PermissionRequestLaunchPlan: Sendable, Equatable {
|
|
|
31
40
|
"--runtime-smoke-completion",
|
|
32
41
|
arguments: arguments
|
|
33
42
|
)
|
|
43
|
+
#if RECORDINGS_BAR_ONLY
|
|
44
|
+
// A bar build is bar-only by construction on EVERY launch path — installer
|
|
45
|
+
// relaunch, manual `open`, LaunchAgent, smoke. Relying on the argument alone left
|
|
46
|
+
// real launches of the installed bar behaviorally identical to the full app
|
|
47
|
+
// (review lineage hasna/apps#269, cycle-1 P1). The explicit --bar-only argument
|
|
48
|
+
// remains a belt-and-suspenders self-describing launch record; it is never the
|
|
49
|
+
// only carrier of the bar property.
|
|
50
|
+
isBarOnly = true
|
|
51
|
+
#else
|
|
52
|
+
isBarOnly = arguments.contains("--bar-only")
|
|
53
|
+
#endif
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
private static func optionValue(_ name: String, arguments: [String]) -> String? {
|