oh-my-codex 0.21.39 → 0.21.40
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/Cargo.lock +6 -6
- package/Cargo.toml +1 -1
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +18 -1
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +11 -3
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +17 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/session-search.js +1 -1
- package/dist/hooks/__tests__/session.test.js +311 -1
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/session.d.ts +9 -8
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +125 -32
- package/dist/hooks/session.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +47 -0
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +2 -2
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +4 -3
- package/dist/mcp/state-paths.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
4
5
|
import { once } from 'node:events';
|
|
5
6
|
import { existsSync } from 'node:fs';
|
|
6
7
|
import { link, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rename, rm, symlink, writeFile } from 'node:fs/promises';
|
|
@@ -68,6 +69,38 @@ async function withOwnerEnvironment(sessionId, run) {
|
|
|
68
69
|
process.env.OMX_SESSION_ID = previous;
|
|
69
70
|
}
|
|
70
71
|
}
|
|
72
|
+
async function withEnvironment(values, run) {
|
|
73
|
+
const previous = new Map(Object.keys(values).map((key) => [key, process.env[key]]));
|
|
74
|
+
for (const [key, value] of Object.entries(values)) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
delete process.env[key];
|
|
77
|
+
else
|
|
78
|
+
process.env[key] = value;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await run();
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
for (const [key, value] of previous) {
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
delete process.env[key];
|
|
87
|
+
else
|
|
88
|
+
process.env[key] = value;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const MANAGED_CODEX_DAEMON_CMDLINE = '/home/test/.codex/packages/app-server/bin/codex app-server --remote-control --listen unix:// --managed-daemon';
|
|
93
|
+
function linuxProcessObservation(cmdline, birth) {
|
|
94
|
+
return {
|
|
95
|
+
kind: 'identity',
|
|
96
|
+
cmdline,
|
|
97
|
+
identity: {
|
|
98
|
+
platform: 'linux',
|
|
99
|
+
birth,
|
|
100
|
+
cmdline_hash: createHash('sha256').update(cmdline).digest('hex'),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
71
104
|
/**
|
|
72
105
|
* Identity fields for a pointer that belongs to the HOST platform.
|
|
73
106
|
*
|
|
@@ -774,7 +807,7 @@ describe('isSessionStale', () => {
|
|
|
774
807
|
}), false);
|
|
775
808
|
});
|
|
776
809
|
});
|
|
777
|
-
describe('
|
|
810
|
+
describe('selected session pointer recovery', { concurrency: false }, () => {
|
|
778
811
|
async function writePointer(cwd, overrides = {}) {
|
|
779
812
|
const context = resolveSessionPointerContext(cwd);
|
|
780
813
|
await mkdir(context.baseStateDir, { recursive: true });
|
|
@@ -789,6 +822,152 @@ describe('verified-dead selected session pointer recovery', { concurrency: false
|
|
|
789
822
|
await writeFile(context.sessionPath, body, 'utf-8');
|
|
790
823
|
return { path: context.sessionPath, body };
|
|
791
824
|
}
|
|
825
|
+
async function writeManagedDaemonPointer(cwd) {
|
|
826
|
+
const context = resolveSessionPointerContext(cwd);
|
|
827
|
+
await mkdir(context.baseStateDir, { recursive: true });
|
|
828
|
+
const body = JSON.stringify({
|
|
829
|
+
session_id: '01a18453-9e49-7c21-9c51-cf411286e627',
|
|
830
|
+
native_session_id: '01a18453-9e49-7c21-9c51-cf411286e627',
|
|
831
|
+
started_at: '2026-10-28T23:03:37.205Z',
|
|
832
|
+
cwd,
|
|
833
|
+
state_root: context.baseStateDir,
|
|
834
|
+
pid: 57617,
|
|
835
|
+
platform: 'linux',
|
|
836
|
+
identity_schema_version: 2,
|
|
837
|
+
process_identity: linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768').identity,
|
|
838
|
+
pid_start_ticks: 87768,
|
|
839
|
+
pid_cmdline: MANAGED_CODEX_DAEMON_CMDLINE,
|
|
840
|
+
});
|
|
841
|
+
await writeFile(context.sessionPath, body, 'utf-8');
|
|
842
|
+
return { path: context.sessionPath, body };
|
|
843
|
+
}
|
|
844
|
+
it('lets resume replace a live managed-daemon owner without inheriting a stale OMX selector', async () => {
|
|
845
|
+
const root = await mkdtemp(join(tmpdir(), 'omx-session-pointer-daemon-resume-'));
|
|
846
|
+
const cwd = join(root, 'workspace');
|
|
847
|
+
const home = join(root, 'home');
|
|
848
|
+
const codexHome = join(root, 'codex-home');
|
|
849
|
+
const omxRoot = join(root, 'omx-root');
|
|
850
|
+
await mkdir(cwd, { recursive: true });
|
|
851
|
+
try {
|
|
852
|
+
await withEnvironment({
|
|
853
|
+
HOME: home,
|
|
854
|
+
CODEX_HOME: codexHome,
|
|
855
|
+
OMX_ROOT: omxRoot,
|
|
856
|
+
OMX_SESSION_ID: 'omx-inherited-stale',
|
|
857
|
+
}, async () => {
|
|
858
|
+
const pointer = await writeManagedDaemonPointer(cwd);
|
|
859
|
+
await withPointerDependencies({
|
|
860
|
+
runtimePlatform: 'linux',
|
|
861
|
+
probePid: () => 'alive',
|
|
862
|
+
observeProcess: () => linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768'),
|
|
863
|
+
}, async () => {
|
|
864
|
+
const context = resolveSessionPointerContext(cwd);
|
|
865
|
+
assert.equal((await readSessionPointer(context)).status, 'non-runtime-owner');
|
|
866
|
+
const established = await establishLaunchSessionBinding(cwd, 'omx-resume-after-daemon');
|
|
867
|
+
assert.equal(established.kind, 'committed-released');
|
|
868
|
+
if (established.kind !== 'committed-released')
|
|
869
|
+
return;
|
|
870
|
+
const resumedState = await readSessionState(cwd);
|
|
871
|
+
assert.equal(resumedState?.session_id, 'omx-resume-after-daemon');
|
|
872
|
+
assert.equal(resumedState?.owner_omx_session_id, undefined, 'unverified inherited OMX_SESSION_ID must not become owner authority');
|
|
873
|
+
assert.equal(await closeLaunchSessionBindingOnce(established.binding).then((result) => result.status), 'closed');
|
|
874
|
+
});
|
|
875
|
+
assert.notEqual(await readFile(pointer.path, 'utf-8'), pointer.body);
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
finally {
|
|
879
|
+
await rm(root, { recursive: true, force: true });
|
|
880
|
+
}
|
|
881
|
+
});
|
|
882
|
+
it('quarantines a live managed-daemon pointer and permits a subsequent launch without stopping the daemon', async () => {
|
|
883
|
+
const root = await mkdtemp(join(tmpdir(), 'omx-session-pointer-daemon-recover-'));
|
|
884
|
+
const cwd = join(root, 'workspace');
|
|
885
|
+
await mkdir(cwd, { recursive: true });
|
|
886
|
+
try {
|
|
887
|
+
await withEnvironment({
|
|
888
|
+
HOME: join(root, 'home'),
|
|
889
|
+
CODEX_HOME: join(root, 'codex-home'),
|
|
890
|
+
OMX_ROOT: join(root, 'omx-root'),
|
|
891
|
+
OMX_SESSION_ID: 'omx-inherited-stale',
|
|
892
|
+
}, async () => {
|
|
893
|
+
const pointer = await writeManagedDaemonPointer(cwd);
|
|
894
|
+
await withPointerDependencies({
|
|
895
|
+
runtimePlatform: 'linux',
|
|
896
|
+
probePid: () => 'alive',
|
|
897
|
+
observeProcess: () => linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768'),
|
|
898
|
+
token: () => SUCCESSOR_TOKEN,
|
|
899
|
+
}, async () => {
|
|
900
|
+
const recovered = await recoverDeadSessionPointer(cwd);
|
|
901
|
+
assert.equal(recovered.status, 'recovered', recovered.reason);
|
|
902
|
+
assert.equal(recovered.recovered, true);
|
|
903
|
+
assert.match(recovered.reason, /managed Codex daemon/);
|
|
904
|
+
assert.ok(recovered.quarantinePath);
|
|
905
|
+
assert.equal(await readFile(recovered.quarantinePath, 'utf-8'), pointer.body);
|
|
906
|
+
assert.equal((await readSessionPointer(resolveSessionPointerContext(cwd))).status, 'absent');
|
|
907
|
+
const established = await establishLaunchSessionBinding(cwd, 'omx-resume-after-recovery');
|
|
908
|
+
assert.equal(established.kind, 'committed-released');
|
|
909
|
+
if (established.kind === 'committed-released') {
|
|
910
|
+
assert.equal(await closeLaunchSessionBindingOnce(established.binding).then((result) => result.status), 'closed');
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
await rm(root, { recursive: true, force: true });
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
it('continues refusing recovery and a fresh launch for a live interactive OMX owner', async () => {
|
|
920
|
+
const root = await mkdtemp(join(tmpdir(), 'omx-session-pointer-interactive-owner-'));
|
|
921
|
+
const cwd = join(root, 'workspace');
|
|
922
|
+
const interactiveCmdline = '/home/test/.local/bin/codex resume active-conversation';
|
|
923
|
+
const interactiveObservation = linuxProcessObservation(interactiveCmdline, '99123');
|
|
924
|
+
await mkdir(cwd, { recursive: true });
|
|
925
|
+
try {
|
|
926
|
+
await withEnvironment({
|
|
927
|
+
HOME: join(root, 'home'),
|
|
928
|
+
CODEX_HOME: join(root, 'codex-home'),
|
|
929
|
+
OMX_ROOT: join(root, 'omx-root'),
|
|
930
|
+
OMX_SESSION_ID: undefined,
|
|
931
|
+
}, async () => {
|
|
932
|
+
const context = resolveSessionPointerContext(cwd);
|
|
933
|
+
await mkdir(context.baseStateDir, { recursive: true });
|
|
934
|
+
const pointerBody = JSON.stringify({
|
|
935
|
+
session_id: 'omx-live-interactive',
|
|
936
|
+
owner_omx_session_id: 'omx-live-interactive',
|
|
937
|
+
launch_lineage_token: TEST_TOKEN,
|
|
938
|
+
started_at: '2026-10-28T23:03:37.205Z',
|
|
939
|
+
cwd,
|
|
940
|
+
state_root: context.baseStateDir,
|
|
941
|
+
pid: 57618,
|
|
942
|
+
platform: 'linux',
|
|
943
|
+
identity_schema_version: 2,
|
|
944
|
+
process_identity: interactiveObservation.identity,
|
|
945
|
+
pid_start_ticks: 99123,
|
|
946
|
+
pid_cmdline: interactiveCmdline,
|
|
947
|
+
});
|
|
948
|
+
await writeFile(context.sessionPath, pointerBody, 'utf-8');
|
|
949
|
+
await withPointerDependencies({
|
|
950
|
+
runtimePlatform: 'linux',
|
|
951
|
+
probePid: () => 'alive',
|
|
952
|
+
observeProcess: () => interactiveObservation,
|
|
953
|
+
}, async () => {
|
|
954
|
+
const established = await establishLaunchSessionBinding(cwd, 'omx-competing-launch');
|
|
955
|
+
assert.equal(established.kind, 'precommit-aborted');
|
|
956
|
+
if (established.kind === 'precommit-aborted')
|
|
957
|
+
assert.equal(established.abort.code, 'session_pointer_owner_conflict');
|
|
958
|
+
const refused = await recoverDeadSessionPointer(cwd);
|
|
959
|
+
assert.equal(refused.status, 'usable');
|
|
960
|
+
assert.equal(refused.recovered, false);
|
|
961
|
+
assert.equal(refused.action, 'none');
|
|
962
|
+
assert.match(refused.reason, /usable or live/);
|
|
963
|
+
});
|
|
964
|
+
assert.equal(await readFile(context.sessionPath, 'utf-8'), pointerBody);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
finally {
|
|
968
|
+
await rm(root, { recursive: true, force: true });
|
|
969
|
+
}
|
|
970
|
+
});
|
|
792
971
|
it('quarantines exact dead evidence, is idempotent, and permits relaunch', async () => {
|
|
793
972
|
const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-recover-dead-'));
|
|
794
973
|
try {
|
|
@@ -3451,6 +3630,137 @@ describe('session pointer transaction', () => {
|
|
|
3451
3630
|
await rm(cwd, { recursive: true, force: true });
|
|
3452
3631
|
}
|
|
3453
3632
|
});
|
|
3633
|
+
it('preserves managed-daemon owner sidecars without publishing them as canonical runtime owners', async () => {
|
|
3634
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-session-daemon-owner-sidecar-'));
|
|
3635
|
+
const nativeSessionId = 'native-daemon-sidecar';
|
|
3636
|
+
const observation = linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768');
|
|
3637
|
+
try {
|
|
3638
|
+
const context = resolveSessionPointerContext(cwd);
|
|
3639
|
+
const sidecarDir = join(context.baseStateDir, 'sessions', nativeSessionId);
|
|
3640
|
+
const sidecarPath = join(sidecarDir, 'session-owner.json');
|
|
3641
|
+
await mkdir(sidecarDir, { recursive: true });
|
|
3642
|
+
await writeFile(sidecarPath, JSON.stringify({
|
|
3643
|
+
session_id: nativeSessionId,
|
|
3644
|
+
native_session_id: nativeSessionId,
|
|
3645
|
+
started_at: '2026-10-28T23:03:37.205Z',
|
|
3646
|
+
cwd,
|
|
3647
|
+
state_root: context.baseStateDir,
|
|
3648
|
+
pid: 57617,
|
|
3649
|
+
platform: 'linux',
|
|
3650
|
+
identity_schema_version: 2,
|
|
3651
|
+
process_identity: observation.identity,
|
|
3652
|
+
pid_start_ticks: 87768,
|
|
3653
|
+
pid_cmdline: MANAGED_CODEX_DAEMON_CMDLINE,
|
|
3654
|
+
}), 'utf-8');
|
|
3655
|
+
await withPointerDependencies({
|
|
3656
|
+
runtimePlatform: 'linux',
|
|
3657
|
+
probePid: () => 'alive',
|
|
3658
|
+
observeProcess: () => observation,
|
|
3659
|
+
}, async () => {
|
|
3660
|
+
assert.equal((await readNativeSessionOwnerEvidence(cwd, nativeSessionId)).status, 'usable');
|
|
3661
|
+
const sidecarBefore = await readFile(sidecarPath, 'utf-8');
|
|
3662
|
+
const owner = await writeNativeSessionOwner(cwd, nativeSessionId, {
|
|
3663
|
+
pid: 57617,
|
|
3664
|
+
platform: 'linux',
|
|
3665
|
+
});
|
|
3666
|
+
assert.equal(owner.session_id, nativeSessionId);
|
|
3667
|
+
const sidecarAfterOwnerWrite = await readFile(sidecarPath, 'utf-8');
|
|
3668
|
+
assert.equal(JSON.parse(sidecarAfterOwnerWrite).session_id, nativeSessionId);
|
|
3669
|
+
await assert.rejects(reconcileNativeSessionStart(cwd, nativeSessionId, {
|
|
3670
|
+
pid: 57617,
|
|
3671
|
+
platform: 'linux',
|
|
3672
|
+
}), (error) => isSessionPointerLaunchAbort(error)
|
|
3673
|
+
&& error.code === 'session_pointer_owner_conflict');
|
|
3674
|
+
assert.equal((await readSessionPointer(context)).status, 'absent');
|
|
3675
|
+
assert.equal(await readFile(sidecarPath, 'utf-8'), sidecarAfterOwnerWrite);
|
|
3676
|
+
assert.notEqual(sidecarAfterOwnerWrite, sidecarBefore);
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
finally {
|
|
3680
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3681
|
+
}
|
|
3682
|
+
});
|
|
3683
|
+
it('P1: rejects SessionStart republishing a managed daemon to the canonical pointer', async () => {
|
|
3684
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-session-daemon-canonical-republish-'));
|
|
3685
|
+
const nativeSessionId = 'native-daemon-codex';
|
|
3686
|
+
const daemonObservation = linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768');
|
|
3687
|
+
try {
|
|
3688
|
+
const context = resolveSessionPointerContext(cwd);
|
|
3689
|
+
// Setup: Create canonical pointer with managed daemon owner (non-runtime-owner status)
|
|
3690
|
+
const pointerPath = join(context.baseStateDir, 'session.json');
|
|
3691
|
+
await mkdir(dirname(pointerPath), { recursive: true });
|
|
3692
|
+
const initialDaemonState = {
|
|
3693
|
+
session_id: 'omx-session-from-daemon',
|
|
3694
|
+
native_session_id: nativeSessionId,
|
|
3695
|
+
started_at: '2026-10-28T23:00:00.000Z',
|
|
3696
|
+
cwd,
|
|
3697
|
+
state_root: context.baseStateDir,
|
|
3698
|
+
pid: 57617,
|
|
3699
|
+
platform: 'linux',
|
|
3700
|
+
identity_schema_version: 2,
|
|
3701
|
+
process_identity: daemonObservation.identity,
|
|
3702
|
+
pid_start_ticks: 87768,
|
|
3703
|
+
pid_cmdline: MANAGED_CODEX_DAEMON_CMDLINE,
|
|
3704
|
+
};
|
|
3705
|
+
await writeFile(pointerPath, JSON.stringify(initialDaemonState), 'utf-8');
|
|
3706
|
+
await withPointerDependencies({
|
|
3707
|
+
runtimePlatform: 'linux',
|
|
3708
|
+
probePid: () => 'alive',
|
|
3709
|
+
observeProcess: () => daemonObservation,
|
|
3710
|
+
}, async () => {
|
|
3711
|
+
// Verify the canonical pointer is classified as non-runtime-owner
|
|
3712
|
+
const canonicalPointerStatus = (await readSessionPointer(context)).status;
|
|
3713
|
+
assert.equal(canonicalPointerStatus, 'non-runtime-owner', 'canonical pointer should be non-runtime-owner before SessionStart');
|
|
3714
|
+
await assert.rejects(reconcileNativeSessionStart(cwd, nativeSessionId, {
|
|
3715
|
+
pid: 57617,
|
|
3716
|
+
platform: 'linux',
|
|
3717
|
+
}), (error) => isSessionPointerLaunchAbort(error)
|
|
3718
|
+
&& error.code === 'session_pointer_unusable'
|
|
3719
|
+
&& error.pointerStatus === 'non-runtime-owner');
|
|
3720
|
+
assert.equal(await readFile(pointerPath, 'utf-8'), JSON.stringify(initialDaemonState));
|
|
3721
|
+
assert.equal((await readSessionPointer(context)).status, 'non-runtime-owner');
|
|
3722
|
+
});
|
|
3723
|
+
}
|
|
3724
|
+
finally {
|
|
3725
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3726
|
+
}
|
|
3727
|
+
});
|
|
3728
|
+
it('retains the OMX owner when managed-daemon SessionStart reconciles a launched pointer', async () => {
|
|
3729
|
+
const cwd = await mkdtemp(join(tmpdir(), 'omx-session-daemon-reconcile-launch-owner-'));
|
|
3730
|
+
const ownerPid = 57616;
|
|
3731
|
+
const daemonPid = 57617;
|
|
3732
|
+
const ownerObservation = linuxProcessObservation('/usr/local/bin/omx', '87767');
|
|
3733
|
+
const daemonObservation = linuxProcessObservation(MANAGED_CODEX_DAEMON_CMDLINE, '87768');
|
|
3734
|
+
try {
|
|
3735
|
+
const context = resolveSessionPointerContext(cwd);
|
|
3736
|
+
await withPointerDependencies({
|
|
3737
|
+
runtimePlatform: 'linux',
|
|
3738
|
+
probePid: () => 'alive',
|
|
3739
|
+
observeProcess: (pid) => pid === daemonPid ? daemonObservation : ownerObservation,
|
|
3740
|
+
}, async () => {
|
|
3741
|
+
const launched = await writeSessionStart(cwd, 'omx-launch-owner', {
|
|
3742
|
+
pid: ownerPid,
|
|
3743
|
+
platform: 'linux',
|
|
3744
|
+
});
|
|
3745
|
+
const reconciled = await reconcileNativeSessionStart(cwd, 'native-daemon-reconcile', {
|
|
3746
|
+
pid: daemonPid,
|
|
3747
|
+
platform: 'linux',
|
|
3748
|
+
});
|
|
3749
|
+
assert.equal(reconciled.session_id, launched.session_id);
|
|
3750
|
+
assert.equal(reconciled.native_session_id, 'native-daemon-reconcile');
|
|
3751
|
+
assert.equal(reconciled.pid, ownerPid);
|
|
3752
|
+
assert.equal(reconciled.process_identity?.birth, ownerObservation.identity.birth);
|
|
3753
|
+
assert.notEqual(reconciled.process_identity?.cmdline_hash, daemonObservation.identity.cmdline_hash);
|
|
3754
|
+
const pointerAfter = await readSessionPointer(context);
|
|
3755
|
+
assert.equal(pointerAfter.status, 'usable');
|
|
3756
|
+
assert.equal(pointerAfter.state?.pid, ownerPid);
|
|
3757
|
+
assert.equal(pointerAfter.state?.native_session_id, 'native-daemon-reconcile');
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
finally {
|
|
3761
|
+
await rm(cwd, { recursive: true, force: true });
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3454
3764
|
it('rejects a symlinked native owner state root', async () => {
|
|
3455
3765
|
const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-root-symlink-'));
|
|
3456
3766
|
try {
|